Thread pools address these issues by reusing a fixed or dynamic set of threads to execute multiple tasks. Instead of creating threads per task, tasks are submitted to a pool, where existing threads pick them up and process them sequentially or in parallel.
Using Built-in Thread Pools
Java’s Executors class provides convenient factory methods to create common thread pool configurations:
package com.example.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) throws InterruptedException {
// Creates a pool that grows as needed, reusing idle threads
ExecutorService pool = Executors.newCachedThreadPool();
// Submit multiple tasks
for (int i = 0; i < 6; i++) {
Thread.sleep(100);
pool.submit(new TaskProcessor());
}
// Optional: shut down gracefully when no longer needed
// pool.shutdown();
}
}
class TaskProcessor implements Runnable {
@Override
public void run() {
for (int step = 1; step <= 100; step++) {
System.out.println(Thread.currentThread().getName() + " processing step: " + step);
}
}
}
While newCachedThreadPool() is useful for short-lived tasks, newFixedThreadPool(n) limits concurrency to n threads, preventing resource exhaustion.
Custom Thread Pool Configuration
For fine-grained control, use ThreadPoolExecutor directly:
package com.example.pool;
import java.util.concurrent.*;
public class CustomThreadPool {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
3, // core pool size
8, // maximum pool size
60L, // keep-alive time for idle threads
TimeUnit.SECONDS, // time unit
new ArrayBlockingQueue<>(5), // bounded task queue
Executors.defaultThreadFactory(), // thread factory
new ThreadPoolExecutor.CallerRunsPolicy() // rejection policy
);
// Submit tasks
for (int i = 0; i < 15; i++) {
executor.submit(new TaskProcessor());
}
// Do not call shutdown() unless intentional termination
}
}
- Core Pool Size: Minimum threads kept alive even when idle.
- Maximum Pool Size: Upper bound on total threads; beyond core size, new threads are created only if the queue is full.
- Task Queue: Holds tasks when all core threads are busy. Use bounded queues to prevent uncontrolled growth.
- Rejection Policies: Options include
AbortPolicy(throws exception),CallerRunsPolicy(executes task in caller thread), and others.
Determining Optimal Parallelism
To align thread pool size with hardware capabilities, query the number of available processors:
package com.example.pool;
public class HardwareAwarePool {
public static void main(String[] args) {
int availableCores = Runtime.getRuntime().availableProcessors();
System.out.println("Available CPU cores: " + availableCores);
// Suggested: use core count as base for fixed thread pool
ExecutorService fixedPool = Executors.newFixedThreadPool(availableCores);
}
}
On a system with 4 physical cores and hyper-threading (8 logical threads), setting the pool size to 8 often yields optimal throughput without excessive context switching.