Last Updated on August 26, 2023 by KnownSense
Java concurrency API includes the executor framework since java 5 to improve the performance of concurrent applications with a lot of concurrent tasks. It is a mechanism that allows you to separate thread creation and management for the implementation of concurrent tasks. You don’t have to worry about the creation and management of threads, only about creating tasks and sending them to the executor.
Basic characteristics of executors
The main characteristics of executors are:
• You don’t need to create any Thread object. If you want to execute a concurrent task, you only create an instance of the task (for example, a class that implements the Runnable interface) and send it to the executor. It will manage the thread that will execute the task.
given a Runnable (an object with a run method) such as this:
public class ReportRunnable implements Runnable {
public void run() {
System.out.println( "Reporting at " + Instant.now() ); // Passing: ( Runnable target , String name )
}
}
…then doing this:
Runnable runnable = new ReportRunnable() ;
thread = new Thread( runnable , "thread-process" );
thread.start();
…is effectively the same as doing this:
ExecutorService executorService = Executors.newSingleThreadExecutor() ;… // You should be keeping a reference to the executor service, so that you can later shut it down gracefully.
Runnable runnable = new ReportRunnable() ;
executorService.submit( runnable ) ;
• Executors reduce the overhead introduced by thread creation reusing the threads. Internally, it manages a pool of threads named worker-threads. If you send a task to the executor and a worker-thread is idle, the executor uses that thread to execute the task.
• It’s easy to control the resources used by the executor. You can limit the maximum number of worker-threads of your executor. If you send more tasks than worker-threads, the executor stores them in a queue. When a worker-thread finishes the execution of a task, it takes another from the queue.
• You have to finish the execution of an executor explicitly. You have to indicate to the executor that it has to finish its execution and kill the created threads. If you don’t do this, it won’t finish its execution and your application won’t end.
Basic components of the executor framework
The executor framework has various interfaces and classes that implement all the functionality provided by executors. The basic components of the framework are:
• The Executor interface: This is the basic interface of the executor framework. It only defines a method that allows the programmer to send a Runnable object to an executor.
• The ExecutorService interface: This interface extends the Executor interface and includes more methods to increase the functionality of the framework, such as the following:
- Execute tasks that return a result: The run() method provided by the Runnable interface doesn’t return a result, but with executors, you can have tasks that return a result
- Execute a list of tasks with a single method call
- Finish the execution of an executor and wait for its termination
• The ThreadPoolExecutor class: This class implements the Executor and ExecutorService interfaces. In addition, it includes some additional methods to get the status of the executor (number of worker-threads, number of executed tasks, and so on), methods to establish the parameters of the executor (minimum and maximum number or worker-threads, time that idle threads will wait for new tasks, and so on) and methods that allow programmers to extends and adapt its functionality.
• The Executors class: This class provides utility methods to create Executor, ExecutorService, ScheduledExecutorService, ThreadFactory, and Callable objects. Some methods are:
- newCachedThreadPool(): This method creates a ThreadPoolExecutor object that reuses a worker-thread if it’s idle, but it creates a new one if it’s necessary. There is no maximum number of worker-threads. These pools will typically improve the performance of programs that execute many short-lived asynchronous tasks. Calls to execute will reuse previously constructed threads if available.
- newSingleThreadExecutor(): This method creates a ThreadPoolExecutor object that uses only a single worker-thread. The tasks you send to the executor are stored in a queue until the worker-thread can execute them. Tasks are guaranteed to execute sequentially, and no more than one task will be active at any given time.
- newFixedThreadPool(int nThreads): Creates a thread pool that reuses a fixed number of threads operating off a shared unbounded queue. At any point, at most nThreads threads will be active processing tasks. If additional tasks are submitted when all threads are active, they will wait in the queue until a thread is available. If any thread terminates due to a failure during execution prior to shutdown, a new one will take its place if needed to execute subsequent tasks. The threads in the pool will exist until it is explicitly shutdown.
- newWorkStealingPool(int parallelism): Creates a thread pool that maintains enough threads to support the given parallelism level, and may use multiple queues to reduce contention. The parallelism level corresponds to the maximum number of threads actively engaged in, or available to engage in, task processing. The actual number of threads may grow and shrink dynamically. A work-stealing pool makes no guarantees about the order in which submitted tasks are executed.
- newSingleThreadScheduledExecutor(): Creates a thread pool that can schedule commands to run after a given delay, or to execute periodically.
Let’s write a simple program to explain working of above classes and interfaces. First, we need to have a Runnable class, named WorkerThread.java
public class WorkerThread implements Runnable{
private String command;
public WorkerThread(String s){
this.command=s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" Start. Command = "+command);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" End.");
}
@Override
public String toString(){
return this.command;
}
}
Then we will create fixed thread pool from Executors framework.
public class SimpleThreadPool {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("" + i);
executor.execute(worker);
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
In the above program, we are creating a fixed-size thread pool of 5 worker threads. Then we are submitting 10 jobs to this pool, since the pool size is 5, it will start working on 5 jobs and other jobs will be in wait state, as soon as one of the job is finished, another job from the wait queue will be picked up by worker thread and get’s executed.
ThreadPoolExecutor provides much more feature than that. We can specify the number of threads that will be alive when we create ThreadPoolExecutor instance and we can limit the size of thread pool and create our own RejectedExecutionHandler implementation to handle the jobs that can’t fit in the worker queue.
public class RejectedExecutionHandlerImpl implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println(r.toString() + " is rejected");
}
}
ThreadPoolExecutor also provide other methods that help in checking current state of the executor, pool size, active thread count and task count