On this page
Thread Pools
Creating a new thread for every task is expensive. Thread pools reuse a fixed set of worker threads, improving performance and resource management.
ExecutorService
The primary interface for submitting tasks to a thread pool:
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> System.out.println("Task running on " + Thread.currentThread().getName()));
Future<Integer> future = executor.submit(() -> 42);
System.out.println(future.get()); // blocks until result is ready
executor.shutdown();
executor.awaitTermination(60, TimeUnit.SECONDS);
Common Thread Pool Types
// Fixed size — best for CPU-bound workloads with known concurrency
ExecutorService fixed = Executors.newFixedThreadPool(4);
// Cached — creates threads as needed, reuses idle threads
ExecutorService cached = Executors.newCachedThreadPool();
// Single thread — sequential execution, tasks queued
ExecutorService single = Executors.newSingleThreadExecutor();
// Scheduled — delayed and periodic tasks
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
scheduled.scheduleAtFixedRate(() -> System.out.println("Tick"), 0, 1, TimeUnit.SECONDS);
Custom ThreadPoolExecutor
For production systems, prefer explicit configuration over Executors factory methods:
ThreadPoolExecutor pool = new ThreadPoolExecutor(
4, // core pool size
8, // maximum pool size
60L, TimeUnit.SECONDS, // keep-alive time
new LinkedBlockingQueue<>(100), // work queue
new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger();
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "worker-" + count.incrementAndGet());
t.setDaemon(false);
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // rejection policy
);
Rejection Policies
| Policy | Behavior |
|---|---|
AbortPolicy |
Throws RejectedExecutionException (default) |
CallerRunsPolicy |
Runs task in the calling thread |
DiscardPolicy |
Silently drops the task |
DiscardOldestPolicy |
Drops oldest queued task |
Sizing Thread Pools
| Workload Type | Suggested Size |
|---|---|
| CPU-bound | Runtime.getRuntime().availableProcessors() |
| I/O-bound | cores * (1 + wait/compute ratio) — often 2× cores or more |
| Mixed | Measure and tune |
int cores = Runtime.getRuntime().availableProcessors();
ExecutorService cpuPool = Executors.newFixedThreadPool(cores);
ExecutorService ioPool = Executors.newFixedThreadPool(cores * 2);
ForkJoinPool
Designed for divide-and-conquer tasks (used by parallel streams):
ForkJoinPool pool = ForkJoinPool.commonPool();
int sum = pool.invoke(new RecursiveTask<Integer>() {
@Override
protected Integer compute() {
return IntStream.range(1, 1000).sum();
}
});
For custom recursive tasks, extend RecursiveTask<V> (returns value) or RecursiveAction (no return value).
CompletableFuture Integration
ExecutorService executor = Executors.newFixedThreadPool(4);
CompletableFuture.supplyAsync(() -> fetchData(), executor)
.thenApplyAsync(data -> process(data), executor)
.thenAcceptAsync(result -> save(result), executor);
Always shut down the executor when done:
executor.shutdown();
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
Best Practices
- Never use
Executors.newCachedThreadPool()in production without bounds — it can create unlimited threads - Always call
shutdown()and handle graceful termination - Use meaningful thread names for debugging
- Prefer
ThreadPoolExecutorwith explicit bounds over unbounded factory methods - Pass a dedicated executor to
CompletableFuture.supplyAsync()instead of using the common pool