Java Executor Framework — All you need to know!
In Java, it has been easy to run a task asynchronously. For example:
class PrintThreadName implements Runnable {
@Override
public void run() {
System.out.println("Thread: " + Thread.currentThread().getName());
}
}
class ThreadTest {
public static void main (String args[]) {
Thread thread = new Thread(new PrintThreadName());
thread.start();
}
}
Output:
Thread: Thread-0
Visualization:
- Now consider that, you want to create 10K threads, then this is a very costly process as we need to create 10K threads and then delete 10K threads.
Instead of this if we go for Java Executor Framework, then you will create only limited threads, for example, 10 Threads (also know as thread pool size) and then submit 10K tasks to it. Here the 10 threads will be reused instead of creating 10K threads.
class ThreadTest {
public static void main (String args[]) {
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new PrintThreadName());
}
}
Output:
Thread: pool-1-thread-1
- Now we can submit tasks to the queue and 10 threads will pick up these tasks.
- It uses a blocking queue as threads pick up tasks concurrently from the queue and the queue has to be thread-safe (Avoid picking a task two times).
What should be the ideal thread pool size?
- Normally, if we have CPU intensive tasks, then the pool size should be equal to the number of CPU cores.
- If we have more threads than the number of CPU cores, the CPU will perform a time split, and allocate some time to each thread.
Code to get the number of CPU cores:
int cpuCoreCount = Runtime.getRuntime().availableProcessors();
If the task is I/O intensive i.e. making DB or HTTP calls, then the number of threads can be more as the thread will be waiting until HTTP/ DB return an answer.
Type of Thread Pool:
- Fixed Thread Pool
ExecutorService service = Executors.newFixedThreadPool(n);
Here “n” is the size of the thread pool. In this type, the pool will be initialized with n thread, user can submit tasks to queue and threads will pick tasks one by one. If all threads are busy tasks will wait until one of the n threads is free to pick up the task.
- Cached Thread Pool
ExecutorService service = Executors.newCachedThreadPool();
In this type, whenever the user submits a task, if threads are busy, then a new thread is created for the task and placed in the pool. If the thread is idle for 60 seconds i.e. no new task to execute then the thread is killed.
- Scheduled Thread Pool
- Single Thread Pool
This type of thread pool is used when you want to ensure that task t1 is run before task t2 and so on.
You can use any one of the predefined ThreadPool according to your need, or you can create your own using the following:
ExecutorService service = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
corePoolSize — base pool size
maxPoolSize — max number of threads that can be reached
keepAliveTime — Time for which a thread can remain before getting killed if the currentPoolSize > corePoolSize
What are the predefined values of the above params in the case of different thread pools:
Assigning tasks to Executor Service:
It can run Runnable or Callable Tasks.
ExecutorService executorService = Executors.newFixedThreadPool(10);
Runnable runnableTask = () -> System.out.println("This is runnable task");
Callable callableTask = () -> "This is callable task";
executorService.execute(runnableTask);
Future<String> future = executorService.submit(callableTask);
Type of Queues:
LinkedBlocking Queue:
Used by Fixed and Single Thread pool as the number of threads are limited and the tasks are unlimited and hence the queue has to be unbounded to store any number of tasks.
Synchronous Queue:
Used by the cached Thread pool. Only a single slot for the task as the threads are not bounded and a new thread is created for each new task.
Delay Queue:
Time delay for tasks.
- What happens if all the threads are busy and the queue also reached its max limit. Then one of the following policies can be applied:
AbortPolicy — It will throw runtime exception (RejectedExecutionException)
DiscardPolicy — New tasks are discarded
DiscardOldest — Add a new task to the queue and discard the oldest task in the queue
CallerRunPolicy — Submitting a new task will run on the caller thread only. This gives buffer as the caller thread itself is busy processing the task and defers from adding new tasks to the queue.
How to shutdown executor service?
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.shutdown();
This will initiate the shutdown process but will not immediately shutdown. Executor service will throw an exception if we submit a task after shutdown.
executorService.isShutdown();
This is used to check if the shutdown began or not.
executorService.isTerminated();
Will return true if all the tasks are complete, even the present in the queue.
I created this article from the notes which I made while preparing for interviews. Any suggestion or edit is highly appreciated. If you like this article consider “FOLLOWING” and giving a “CLAP”.