Java Programs›Multithreading›Implement a Thread Pool

Implement a Thread Pool in Java

advanced·  Multithreading  ·  Concurrency

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.

Input
8 tasks computing the squares of 1 to 8, 3 workers
Output
Squares: [1, 4, 9, 16, 25, 36, 49, 64]

Java Program

Java
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

3 worker threads ran 8 tasks Squares: [1, 4, 9, 16, 25, 36, 49, 64]

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.

How It Works
  1. 1The constructor starts size workers, each blocking on tasks.take() while the queue is empty.
  2. 2submit just adds the task to the queue; whichever worker is free picks it up.
  3. 3shutdownAndWait queues one STOP marker per worker after all real tasks, then joins every worker.
  4. 4Each task writes to its own slot in an AtomicLongArray, so main can print the results after the workers finish.
With three workers and eight tasks, each worker runs two or three tasks one after another instead of eight separate threads being created.
💡

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

worker threadsBlockingQueuepoison pillAtomicLongArray

Approach 2: Configuring ThreadPoolExecutor

Java
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

Results: 10 20 30 40 50 Terminated cleanly: true

Core Logic

In real code the JDK's ThreadPoolExecutor does all of this, and its constructor exposes every knob the simple pool hard-codes.

How It Works
  1. 1Core size 2 keeps two threads alive; maximum 4 allows extra threads only when the queue is full.
  2. 2An ArrayBlockingQueue of 10 bounds how many tasks can wait.
  3. 3CallerRunsPolicy makes the submitting thread run a task itself when the pool and queue are full, which naturally slows producers down.
  4. 4shutdown plus awaitTermination stops the pool cleanly.
Five small tasks fit easily in the queue, so they run on the two core threads and their results print in submission order.
💡

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

ThreadPoolExecutorcore and maximum pool sizerejection policy

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).

Learn the Concept

Related Programs