AndroidPub
Published in

AndroidPub

Executor Framework- Understanding the basics (Part 1)

Prior to Java 5, achieving concurrency was a difficult task for developers. But thanks to the introduction of the very powerful Executor framework, developers can do concurrent programming with much ease. Lets see the advantages of this framework.

  1. The most important feature of this framework is the separation of concerns. It lets the developer to create tasks(Runnables, Callables), and let the framework decide when, how and where to execute that task on a Thread which is totally configurable.
  2. It relieves the developer from thread management.
  3. It provides the developers various types of queues for storing the tasks. It also provides various mechanisms for handling the scenario in which a task is rejected by the queue when it is full.

Let me explain this framework with a very relatable scenario.

Ever been to a railway station to get a ticket? There are various counters for issuing tickets. If the counter is free, you get the ticket. But if the counter is not free, you stand in a queue.

Now let me translate the above scenario in terms of this framework.

  1. For you and for all those people who want to get a ticket, getting a ticket is a task ( Runnable, Callable).
  2. The counters who are issuing the tickets are working to issue the tickets. They are Worker Threads and if we see them as one entity they are a ThreadPool.
  3. The people who are waiting to get a ticket are standing in a queue.

How does Executor work?

To simply put, the work of an executor is to execute tasks . The executor picks up a thread from the threadpool to execute a task. If a thread is not available and new threads cannot be created, then the executor stores these tasks in a queue. A task can also be removed from the queue. If the queue is full, then the queue will start rejecting the tasks.(Rejection of tasks can be handled).

The below diagram summarizes the work of the Executor framework.

Note: Red color means busy/full and Green color species empty/idle

After the task is completed, the framework will not terminate the executing thread immediately. The executor keeps a minimum number of threads in the thread pool even if all of them are not executing some task. But it will terminate the extra threads (number of threads which are greater than the minimum number of threads) after the specified duration.

Using ThreadPoolExecutor

Executor is the base interface of this framework which has only one method execute(Runnable command). ExecutorService and ScheduledExecutorService are two other interfaces which extends Executor. These two interfaces have a lot of important methods like submit(Runnable task), shutdown(), schedule(Callable<V> callable,long delay, TimeUnit unit) etc which actually make this framework really useful. The most commonly used implementations of these interfaces are ThreadPoolExecutor and ScheduledThreadPoolExecutor. You can find other implementations here.

While creating an instance of ThreadPoolExecutor, we can give various configurations. We specify the minimum and maximum number of threads that should be present in the ThreadPool. We also specify the duration after which the extra threads(current threads — minimum threads) should be terminated. There are various types of queues which can be used for storing the tasks which cannot be executed immediately. Based on the application requirements we specify the type of queue to be used. Below is the code snippet for creating an instance of ThreadPoolExecutor.

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {

Now lets understand the parameters of ThreadPoolExecutor. For optimal performance of the application, each parameter to the executor needs to be passed very carefully.

corePoolSize : The minimum number of threads to keep in the pool.
maximumPoolSize : The maximum number of threads to keep in the pool.
keepAliveTime : If current number of threads are greater than the minimum threads, then wait for this time to terminate the extra threads.
unit: The time unit for the previous argument.
workQueue : The queue used for holding the tasks.
handler : An instance of RejectionExecutionHandler, which handles the task which is rejected by the executor.

Once the executor is created, it is ready to accept tasks for execution. We can execute tasks through these two methods:
execute(Runnable command): This method accepts a Runnable object but returns void.
submit(Runnable task)/submit(Callable<T> task): This method can accept a Runnable or Callable object and returns Future object.

For demonstrating the workings of ThreadPoolExecutor, I will be using a method will takes integer as an argument and returns a random alpha-numeric string of that length. The method internally invokes Thread.sleep(1000). So there will be a delay of 1 second for every response.

Let’s start with a very simple example. In the below code, an executor is created with 2 minimum threads and 2 maximum threads. The queue used is LinkedBlockingQueue. We call printString() function which creates 6 Runnables in a loop and passes them on for execution. Each Runnable calls getRandomString(int length) function of the above class with the current index of the loop as ‘length’ variable. So first Runnable will get String of length 1, second Runnable will get String of length 2 and so on.

Although there are 6 runnables, the executor has only 2 threads to execute these runnables. So only 2 runnables will be executed at a time. Once their execution is completed, the threads will pick up next 2 Runnables for execution and so on until all the tasks are completed.

The output of the above program is

— — — — — — — — — — — — Delay of 1 second — — — — — — — ——
String returned is : g
String returned is
: io
— — — — — — — — — — — — Delay of 1 second — — — — — — — — — —
String returned is : k9k
String returned is
: o6zh
— — — — — — — — — — — — Delay of 1 second — — — — — — — — — —
String returned is : mujt5
String returned is
: xn4gar

Output will vary because I am returning a Random string.

By changing the thread configuration, we can change the output, If the executor is created with 3 minimum and 3 maximum threads, then 3 Runnables will be executed in parallel, And the output will be somewhat like this.

— — — — — — — — — — — — Delay of 1 second — — — — — — — — — — String returned is : g
String returned is
: io
String returned is
: k9k
— — — — — — — — — — — — Delay of 1 second — — — — — — — ——
String returned is : o6zh
String returned is
: mujt5
String returned is
: xn4gar

Using ScheduledThreadPoolExecutor

This executor can be used when we want to schedule the tasks. There are times when we want to the repeat the execution of a same task at certain time intervals- e.g. Executing a task after every 5 seconds. ScheduledThreadPoolExecutor is the perfect candidate for these types of tasks. There are various useful methods for scheduling tasks in this executor as explained below.

schedule(Runnable command, long delay, TimeUnit unit) : This will execute the task after the delay has expired. So let’s say that you have to execute a task after 10 seconds use schedule(runnable, 10, TimeUnit.SECONDS).

scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) : For the first time, the runnable will be executed after the initialDelay has expired. After that, every time that Runnable will be run after the given period (3rd argument) has expired. Let’s say that you want to run a task after every 10 seconds. Using schedule(runnable, 0, 10, TimeUnit.SECONDS) , the Runnable will execute immediately for the first time, and from there on it will be executed after every 10 seconds. The time interval between executing tasks does not depend upon the time taken by the task itself to complete.

scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) : Although this function may seem similar to the above function, there is a difference between them. Unlike the previous function, this function waits for the previously executing task to complete and only after delay period (3rd argument) has expired, it will execute the task next time.

Shutting down an Executor

An executor can be shut down using shutDown() function. When the executor is shut down, it will no longer accept any new task and submitting any task to it will throw a RejectedExecutionException. But the tasks already executing on the threads and stored in the queue for execution will be executed. But if we do not want to execute the tasks stored in queue, then we need to call shutDownNow(). Calling this method will make sure that any task stored in the queue should be not executed.

What about the tasks which are already executing on the thread? Looks like calling any of the above two methods won’t cancel those tasks. But the executor sends an interrupt flag to all the currently executing threads. The runnable should check whether the thread is interrupted or not. And if the thread is interrupted, the runnable should decide what to do.

public static String getRandomString(int length) {  if(Thread.currentThread().isInterrupted()){
// Either ignore this block or do some action
}
}

Thanks for reading this article. I hope that by reading this article, you got a better understanding of the Executor framework. My next article about Executors is about using different types of queues for different purposes.

Thanks for reading out the article.

Check out my other Github projects here and my other medium articles here. Also, let’s get connected on LinkedIn.

--

--

The (retired) Pub(lication) for Android & Tech, focused on Development

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Anshul Jain

Anshul Jain

ex Android Developer | Machine Learning Developer @ Dailyhunt