Concurrency in Java: Thread pools, ExecutorService & Future
- 4.1/5
- 3817
- Jul 20, 2024
A thread pool is a set of pre-allocated threads that are adept at executing tasks on demand.
Java threads are mapped to system-level threads, hence consuming the operating system's resources.
If we create threads uncontrollably, we may run out of these resources quickly. A thread pool helps save these system resources in a multithreaded application.
Using a thread pool, an application does not need to create a new thread each time a thread is required; this can significantly reduce resource consumption.
Why a thread pool?
Using a thread pool can offer a number of advantages over creating and managing threads yourself.
1) A thread pool provides a solution to the problem of thread cycle overhead by using previously created threads to execute current tasks.
2) Thread pools also provide a significant advantage by separating the execution of tasks from the creation and management of threads.
3) A thread pool can also be used for executing tasks in a controlled manner; for example, by limiting the number of concurrent executions or prioritizing certain tasks over others.
4) Thread pools can use a queue for tasks, which can help ensure that critical tasks are not delayed due to a lack of available threads.
Thread Pools in Java
The java.util.concurrent package provides a number of classes and interfaces that can be used to create a variety of "thread pools" in Java.
1) Executor interface
The "java.util.concurrent.Executor" interface has a single "void execute(Runnable command)" method to submit a Runnable instance for execution.
The command may execute in a new thread, in a pooled thread, or in the calling thread, at the discretion of the Executor implementation.
The "Executors" class provides convenient factory methods for these Executors.
pool-1-thread-1 Hello A !!! pool-1-thread-1 Hello B !!!
In the code above, Executors.newSingleThreadExecutor() returns an instance of the implementation of "ExecutorService". The "ExecutorService" interface in turn extends the "Executor" interface itself.
2) ExecutorService interface
The "java.util.concurrent.ExecutorService" interface extends the "Executor" interface and also adds methods that help manage and control the execution of threads.
pool-1-thread-3 running !!! pool-1-thread-2 running !!! pool-1-thread-1 running !!!
In the code above, Executors.newFixedThreadPool(5) returns an instance of the "ThreadPoolExecutor" class.
For this instance of "ThreadPoolExecutor", the value for "corePoolSize" and "maximumPoolSize" is set to "5", and the values for other parameters are set to default, i.e., keepAliveTime=0, unit=miliseconds, workingQueue=LinkedBlockingQueue
The "ThreadPoolExecutor" class has different constructors, hence other factory methods in "Executors" can replace or change the above-mentioned default values.
keepAliveTime
Threads use this timeout when there are more threads than "corePoolSize" present or if "allowCoreThreadTimeOut" is set to "true". Otherwise, they wait forever for new work.
corePoolSize
Core pool size is the minimum number of workers to keep alive (and not allow to time out, etc.) unless allowCoreThreadTimeOut is set, in which case the minimum is zero.
maximumPoolSize
Maximum pool size.
2.1) Single thread pool
The "single thread pool" is an implementation of "ThreadPoolExecutor" that contains a single thread. In this, the "ThreadPoolExecutor" is decorated with an immutable wrapper, so it can't be reconfigured after creation.
In this thread pool, tasks are guaranteed to execute sequentially, and no more than one task will be active at any given time.
pool-1-thread-1 running !!! pool-1-thread-1 running !!! pool-1-thread-1 running !!!
In the code above, Executors.newSingleThreadExecutor() returns a "wrapper" (i.e. FinalizableDelegatedExecutorService) instance of the "ThreadPoolExecutor" class with "corePoolSize" and "maximumPoolSize" set to "1".
2.2) Fixed thread pool
The "fixed thread pool" is an implementation of "ThreadPoolExecutor" that contains a specified fixed number of threads.
pool-1-thread-1 running !!! pool-1-thread-3 running !!! pool-1-thread-2 running !!!
In the code above, Executors.newFixedThreadPool(5) returns an instance of the "ThreadPoolExecutor" class with "corePoolSize" and "maximumPoolSize" set to "5".
2.3) Cached thread pool
The "cached thread pool" is an implementation of "ThreadPoolExecutor" and does not accept any parameter for the number of threads.
It creates new threads as needed but will reuse previously constructed threads when they are available. Threads that have not been used for "sixty seconds" are terminated and removed from the cache.
pool-1-thread-1 running !!! pool-1-thread-2 running !!! pool-1-thread-3 running !!!
In the code above, Executors.newCachedThreadPool() returns an instance of the "ThreadPoolExecutor" class with "corePoolSize=0", "maximumPoolSize=Integer.MAX_VALUE" and workingQueue=new SynchronousQueue
In a SynchronousQueue, pairs of "insert" and "remove" operations always occur simultaneously. So, the queue never actually contains anything, and its size will always be zero.
3) ScheduledExecutorService interface
The "ScheduledExecutorService" interface extends the "ExecutorService" interface and also provides additional methods that can schedule commands to run after a given delay or to execute periodically.
The implementing class of "ScheduledExecutorService" is "ScheduledThreadPoolExecutor".
1) The method "public ScheduledFuture> schedule(Runnable command, long delay, TimeUnit unit)" allows a task to run once after a specified delay.
2) The method "public ScheduledFuture> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)" allows a task to run after a specified initial delay and then run it repeatedly with a certain period.
3) The method "public ScheduledFuture> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)" is similar to scheduleAtFixedRate, but the specified delay is measured from the end of the previous task.
4) ForkJoinPool
The fork/join framework was introduced in Java 7; ForkJoinPool is the central part of this framework.
The "ForkJoinPool" is an implementation of the "ExecutorService" that manages worker threads and provides us with tools to get information about the thread pool's state and performance.
It implements the work-stealing algorithm (free threads try to "steal" work from busy threads).
The framework first "forks" or recursively breaks the task into smaller independent subtasks until they are simple enough to run asynchronously.
Next, the "join" part begins, in which the results of all subtasks are recursively joined into a single result. In the case of a task that returns void, the program simply waits until every subtask runs.
In Java 8, the most convenient way to get access to the instance of the ForkJoinPool is to use its static method commonPool().
Available cores: 8 Thread: main Thread: main Thread: ForkJoinPool.commonPool-worker-1 Thread: ForkJoinPool.commonPool-worker-2 Thread: ForkJoinPool.commonPool-worker-1 Thread: ForkJoinPool.commonPool-worker-1 Result: 150 Pool Size: 5
Future
The java.util.concurrent.Future class represents a future result of an asynchronous computation that will eventually appear in the 'Future' after the processing is complete.
areaFutureA calculation is running ... and areaFutureB calculation is running ... areaFutureA calculation is running ... and areaFutureB calculation is running ... areaFutureA calculation is running ... and areaFutureB calculation is running ... areaFutureA calculation is running ... and areaFutureB calculation is running ... AreaA: 200, AreaB: 375 Calculation is complete !!!
boolean isDone()
It tells us if the executor has finished processing the task and returns "true" or "false" accordingly.
V get()
It returns the actual result from the calculation. The method get() blocks the execution until the task is complete.
V get(long timeout, TimeUnit unit)
It is an overloaded version that takes a timeout and a TimeUnit as arguments. It throws a TimeoutException if the task doesn't return before the specified timeout period.
boolean cancel(boolean mayInterruptIfRunning)
It attempts to cancel the execution of this task. This method has no effect if the task has already been completed, cancelled, or could not be cancelled for some other reason.
If the task has not started when cancel is called, it should never run.
The "mayInterruptIfRunning" parameter, if "true,"tries to interrupt the thread executing the task (if the thread is known to the implementation), otherwise, in-progress tasks are allowed to complete.
It also returns a boolean value: "false" if the task could not be cancelled; "true" otherwise.