Codectionary / Developer documentation / Java

ExecutorService & Thread Pools

ExecutorService, from java.util.concurrent, manages a pool of reusable threads, letting you submit tasks without manually creating and managing individual Thread objects. This avoids the overhead of constantly creating new threads and gives you control over how many run concurrently. Executors provides convenient factory methods for common pool configurations, like a fixed-size pool or a single-threaded executor.

Syntax

ExecutorService executor = Executors.newFixedThreadPool(n);

Examples

Basic ExecutorService Usage

Submitting tasks to a thread pool instead of manually managing Thread objects.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

ExecutorService executor = Executors.newFixedThreadPool(3);

for (int i = 1; i <= 5; i++) {
    int taskId = i;
    executor.submit(() -> {
        System.out.println("Running task " + taskId + " on " + Thread.currentThread().getName());
    });
}

executor.shutdown();  // stop accepting new tasks, let submitted ones finish

Getting Results with Future

submit() returns a Future, letting you retrieve a task's result once it completes.

import java.util.concurrent.*;

ExecutorService executor = Executors.newFixedThreadPool(2);

Future<Integer> future = executor.submit(() -> {
    Thread.sleep(100);
    return 42;
});

try {
    Integer result = future.get();  // blocks until the task finishes
    System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
    System.out.println("Task failed: " + e.getMessage());
}

executor.shutdown();

Different Executor Types

Executors provides several pool configurations for different needs.

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

ExecutorService fixedPool = Executors.newFixedThreadPool(4);   // exactly 4 threads
ExecutorService cachedPool = Executors.newCachedThreadPool();   // grows/shrinks as needed
ExecutorService singleThread = Executors.newSingleThreadExecutor(); // one thread, tasks run in order

singleThread.submit(() -> System.out.println("Task A"));
singleThread.submit(() -> System.out.println("Task B"));  // guaranteed to run after A

fixedPool.shutdown();
cachedPool.shutdown();
singleThread.shutdown();

Best practices

  • Always call shutdown() (or shutdownNow() for immediate termination) when done with an ExecutorService - forgetting to do so leaves threads running indefinitely
  • Use newFixedThreadPool() for a predictable, bounded number of concurrent tasks, protecting your system from unbounded thread creation
  • Handle both InterruptedException and ExecutionException when calling future.get(), since either can occur if the task fails or is interrupted
  • Prefer ExecutorService over manually creating raw Thread objects for any real application - it is more efficient and easier to manage correctly

At a glance

Purpose
General-purpose application development
File extension
.java
Runs in
Java Virtual Machine
Usually used with
JDK and Java libraries

Specifications & further reading

Related Java documentation

Creating Threads (Thread & Runnable)
A thread is an independent path of execution within a program, letting multiple tasks run seemingly simultaneously. Java offers two main ways to create a thread: extending the Thread class directly and overriding its run() method, or implementing the Runnable interface and passing it to a Thread - the Runnable approach is generally preferred, since Java doesn't support multiple inheritance and Runnable keeps your class free to extend something else.
synchronized Keyword
The synchronized keyword prevents multiple threads from executing a block of code (or a method) at the same time on the same object, protecting shared, mutable data from race conditions. When one thread enters a synchronized block, it acquires a lock on the specified object; any other thread trying to enter a synchronized block on that same object must wait until the lock is released.
Atomic Variables
Classes like AtomicInteger, AtomicLong, and AtomicBoolean, from java.util.concurrent.atomic, provide thread-safe operations on single variables without needing explicit synchronized blocks. They use low-level, lock-free CPU instructions (compare-and-swap) to guarantee that operations like incrementAndGet() are atomic - performed as a single, uninterruptible step - even when many threads use them concurrently.
Thread Control: sleep, join, interrupt
Java provides several methods for coordinating and controlling thread execution. Thread.sleep() pauses the current thread for a specified duration. join() makes the calling thread wait until another thread finishes before continuing. interrupt() signals a thread that it should stop what it's doing, a cooperative mechanism the target thread must actively check for and respond to.