When developing a multi-threaded application, we must usually deal with organizing the work of threads. The larger our application and the more threads we need for multithreaded tasks, the more Runnable objects we create.
It should be noted here that creating a thread in Java is a rather expensive operation. If we create a new instance of the thread each time to perform an operation, we will get big problems with performance and, as a result, with the health of the application.
A thread pool and ThreadPoolExecutor come to our aid here.
A thread pool is a set of pre-initialized threads. Its size can be fixed or variable.
If there are more tasks than threads, then tasks wait in a task queue. The Nth thread in the pool takes a task from the queue, and after it is done, the thread picks up a new task from the queue. Once all tasks in the queue are executed, the threads remain active and wait for new tasks. When new tasks appear, the threads start executing them as well.
Starting with Java 5, the Executor framework gained a multithreading solution. In general, it has lots of components and its purpose is to help us efficient manage queues and thread pools.
The main interfaces are Executor and ExecutorService.
Executor is an interface with a single void execute(Runnable runnable) method.
When passing a task to an implementation of this method, know that it will be executed asynchronously in the future.
ExecutorService — An interface that extends the Executor interface, adding capabilities for executing tasks. It also has methods for interrupting a running task and terminating the thread pool.
ThreadPoolExecutor implements the Executor and ExecutorService interfaces and separates task creation from task execution. We need to implement Runnable objects and send them to an executor. The ThreadPoolExecutor is then responsible executing the tasks, and creating and working with threads.
After a task is sent for execution, an existing thread in the pool is used. This improve performance. It solves the problem of wasting resources on creating and initializing a new thread, and then again on garbage collection once we're done with the thread.
ThreadPoolExecutor has 4 constructors:
ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
The ThreadPoolExecutor constructor has the following parameters:
|corePoolSize||This parameter indicates how many threads will be ready (started) when the executor service starts.|
|maximumPoolSize||The maximum number of threads that an executor service can create.|
|keepAliveTime||The time that a freed thread will continue to live before being destroyed if the number of threads is greater than corePoolSize. The time units are specified in the next parameter.|
|unit||Time units (hours, minutes, seconds, milliseconds, etc.).|
|workQueue||Implementation of a queue for tasks.|
|handler||Handler for tasks that cannot be completed.|
|threadFactory||An object that creates new threads on demand. Using thread factories makes calls to a new thread hardware independent, allowing applications to use special thread subclasses, priorities, and so on.|
Creating a ThreadPoolExecutor
The Executors utility class can simplify creation of a ThreadPoolExecutor. The methods of this utility class help us prepare a ThreadPoolExecutor object.
|newFixedThreadPool — Creates a thread pool that reuses a fixed number of threads to execute any number of tasks.||
|newWorkStealingPool — Creates a thread pool where number of threads is equal to the number of processor cores available to the JVM. The default concurrency level is one. This means that as many threads will be created in the pool as there are CPU cores available to the JVM. If the concurrency level is 4, then the passed value is used instead of the number of cores.||
|newSingleThreadExecutor — Creates a pool with a single thread to execute all tasks.||
|newCachedThreadPool — Creates a thread pool that creates new threads as needed, but reuses previously created threads when they are available.||
|newScheduledThreadPool — Creates a thread pool that can schedule commands to execute after a given delay or periodically.||
We will consider each type of pool in the following lessons.