Implement a Thread Pool in Java
Problem
A thread pool keeps a fixed set of worker threads alive and feeds them tasks from a queue, avoiding the cost of creating a new thread for every task.
Implement a pool of three worker threads that runs eight tasks, then shuts down cleanly once every task is finished.
Java Program
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicLongArray;
public class SimpleThreadPool {
private final BlockingQueue<Runnable> tasks = new LinkedBlockingQueue<>();
private final Thread[] workers;
private static final Runnable STOP = () -> { }; // poison pill, one per worker
SimpleThreadPool(int size) {
workers = new Thread[size];
for (int i = 0; i < size; i++) {
workers[i] = new Thread(() -> {
try {
while (true) {
Runnable task = tasks.take(); // idle workers wait here
if (task == STOP) return;
task.run(); // the same thread runs task after task
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
workers[i].start();
}
}
void submit(Runnable task) {
tasks.add(task);
}
void shutdownAndWait() throws InterruptedException {
for (int i = 0; i < workers.length; i++) tasks.add(STOP); // queued after all real tasks
for (Thread w : workers) w.join();
}
public static void main(String[] args) throws InterruptedException {
SimpleThreadPool pool = new SimpleThreadPool(3);
AtomicLongArray squares = new AtomicLongArray(8); // each task writes its own slot
for (int i = 0; i < 8; i++) {
final int n = i + 1;
pool.submit(() -> squares.set(n - 1, (long) n * n));
}
pool.shutdownAndWait();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < squares.length(); i++) sb.append(i == 0 ? "" : ", ").append(squares.get(i));
System.out.println("3 worker threads ran 8 tasks");
System.out.println("Squares: [" + sb + "]");
}
}Output
Core Logic
Each worker loops forever, taking the next task from a shared queue and running it, so a handful of threads can process any number of tasks.
- 1The constructor starts
sizeworkers, each blocking ontasks.take()while the queue is empty. - 2
submitjust adds the task to the queue; whichever worker is free picks it up. - 3
shutdownAndWaitqueues oneSTOPmarker per worker after all real tasks, then joins every worker. - 4Each task writes to its own slot in an
AtomicLongArray, so main can print the results after the workers finish.
Key Point: Because the stop markers are queued after the real tasks, every submitted task runs before the workers exit — the same guarantee ExecutorService.shutdown() gives.
Key Concepts
Approach 2: Configuring ThreadPoolExecutor
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorConfig {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, // core threads kept alive
4, // maximum threads under load
30, TimeUnit.SECONDS, // idle time before extra threads stop
new ArrayBlockingQueue<>(10), // bounded queue of waiting tasks
new ThreadPoolExecutor.CallerRunsPolicy()); // if full, the caller runs the task itself
List<Future<Integer>> results = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
final int n = i;
results.add(pool.submit(() -> n * 10));
}
StringBuilder line = new StringBuilder();
for (Future<Integer> f : results) {
line.append(line.length() == 0 ? "" : " ").append(f.get()); // get() waits for each result in order
}
System.out.println("Results: " + line);
pool.shutdown();
System.out.println("Terminated cleanly: " + pool.awaitTermination(5, TimeUnit.SECONDS));
}
}
Output
Core Logic
In real code the JDK's ThreadPoolExecutor does all of this, and its constructor exposes every knob the simple pool hard-codes.
- 1Core size 2 keeps two threads alive; maximum 4 allows extra threads only when the queue is full.
- 2An
ArrayBlockingQueueof 10 bounds how many tasks can wait. - 3
CallerRunsPolicymakes the submitting thread run a task itself when the pool and queue are full, which naturally slows producers down. - 4
shutdownplusawaitTerminationstops the pool cleanly.
Key Point: Executors.newFixedThreadPool uses an unbounded queue, which can grow until memory runs out under heavy load; constructing ThreadPoolExecutor directly lets you bound it.
Key Concepts
Interview Questions
Why not create a new thread for every task?
Creating and destroying threads is expensive, and thousands of threads exhaust memory. A pool reuses a fixed number of threads.
How do you choose a thread pool size?
For CPU-bound work, about the number of cores. For tasks that mostly wait on I/O, more threads help, roughly cores × (1 + wait time ÷ compute time).