Task Queues in Java: ExecutorService and ThreadPoolExecutor
In multithreaded Java applications, managing tasks and their execution is one of the key challenges. Instead of creating a new thread for each task (which is expensive and inefficient), developers use task queues in combination with thread pools. This allows controlling the number of simultaneously running threads, reusing them, and smoothly handling peak loads.
The foundation for working with task queues in Java is the java.util.concurrent framework, introduced in Java 5. The main components are: ExecutorService, ThreadPoolExecutor, and various implementations of BlockingQueue.
1. ExecutorService — High-Level Interface
ExecutorService is an interface that provides methods for asynchronous task execution. It hides the details of thread management. The most popular way to obtain an instance is to use the factory methods of the Executors class:
ExecutorService executor = Executors.newFixedThreadPool(4); // pool of 4 threadsexecutor.submit(() -> { System.out.println("Task is running in thread: " + Thread.currentThread().getName());});executor.shutdown(); // shutdown after all tasks completeHowever, factory methods have limited configuration capabilities. For fine-tuning, ThreadPoolExecutor is used directly.
2. ThreadPoolExecutor — Configurable Thread Pool
ThreadPoolExecutor is the core of the framework. It accepts a task queue, saturation policy, thread lifetime, and other parameters. The full constructor:
public ThreadPoolExecutor( int corePoolSize, // minimum number of threads int maximumPoolSize, // maximum number of threads long keepAliveTime, // thread idle time before removal TimeUnit unit, // time unit BlockingQueue<Runnable> workQueue, // task queue RejectedExecutionHandler handler // policy for overflow);Example of creating a pool with a custom queue:
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 4, 30, TimeUnit.SECONDS, queue, new ThreadPoolExecutor.AbortPolicy() // default — throws exception);executor.execute(() -> { // heavy task});executor.shutdown();3. BlockingQueue — The Foundation of Task Queues
The type of queue determines how tasks will be stored and passed to threads. The most popular implementations:
LinkedBlockingQueue— an unbounded queue based on a linked list. If capacity is not set, it can lead to memory overflow.ArrayBlockingQueue— a bounded queue based on an array. Allows controlling the number of waiting tasks.SynchronousQueue— a zero-capacity queue. Each task is passed directly to a thread, no waiting in the queue.PriorityBlockingQueue— a priority queue. Tasks are sorted by priority (implementComparable).
Example with a bounded queue and rejection policy:
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 5, 10, TimeUnit.SECONDS, queue, new ThreadPoolExecutor.CallerRunsPolicy() // task runs in the calling thread);for (int i = 0; i < 20; i++) { executor.submit(() -> { Thread.sleep(100); return Math.random(); });}executor.shutdown();4. Saturation Policies (RejectedExecutionHandler)
When the queue is over