Thread Pool in Java

Java has built-in thread pools in java.util.concurrent. Use ExecutorService instead of creating a new Thread per task — workers are reused from a pool. See What is Thread Pool?.

Manual threads vs thread pool

Without a pool you create each worker yourself — e.g. four separate Thread objects — and assign tasks to them by hand. That works for a tiny, fixed workload but breaks down quickly.

Thread pool (newFixedThreadPool(4)) Four separate threads (new Thread(...))
Submit 10 tasks → 4 workers reuse themselves; extra tasks wait in queue automatically.

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

public class FixedPoolExample {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 10; i++) {
            int taskId = i;
            pool.submit(() ->
                System.out.println("task " + taskId
                    + " on " + Thread.currentThread().getName())
            );
        }

        pool.shutdown(); // one call — pool drains queue and stops
    }
}
            
Only 4 threads exist; each needs manual start() / join(). No shared queue.

public class ManualFourThreads implements Runnable {
    private int taskId;

    public void setTask(int id) { this.taskId = id; }

    public void run() {
        System.out.println("task " + taskId
            + " on " + Thread.currentThread().getName());
    }

    public static void main(String[] args) throws InterruptedException {
        // 4 OS threads created manually — one object per thread
        ManualFourThreads worker = new ManualFourThreads();
        Thread t1 = new Thread(worker, "worker-1");
        Thread t2 = new Thread(worker, "worker-2");
        Thread t3 = new Thread(worker, "worker-3");
        Thread t4 = new Thread(worker, "worker-4");

        // YOU must assign each task and start/join each thread
        worker.setTask(0); t1.start(); t1.join();
        worker.setTask(1); t2.start(); t2.join();
        worker.setTask(2); t3.start(); t3.join();
        worker.setTask(3); t4.start(); t4.join();
        // tasks 4–9? create more threads or rewrite scheduling logic
    }
}
            

Issues with four separate threads

a. Threads die after join(). A pool keeps workers alive and pulls the next job from the queue.
b. No central shutdown — pool: shutdown() / awaitTermination() once. Manual: track every thread, join or interrupt each, handle partial failure.
c. Hard to cap concurrency — pool size = 4 enforces the limit. With manual threads, a bug that calls start() on extra threads bypasses the cap.
d. More tasks than threads — 10 tasks, 4 threads: pool queues 6 automatically. Manual approach requires batching, another queue, or creating 6 more threads.

1. Fixed thread pool — newFixedThreadPool

Fixed number of worker threads. Extra tasks wait in an unbounded queue. Prefer this over creating Thread t1…t4 by hand.


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

public class FixedPoolExample {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 10; i++) {
            int taskId = i;
            pool.submit(() ->
                System.out.println("task " + taskId + " on " + Thread.currentThread().getName())
            );
        }

        pool.shutdown(); // no new tasks; finish existing ones
    }
}

// Example output (order varies):
// task 0 on pool-1-thread-1
// task 1 on pool-1-thread-2
// task 4 on pool-1-thread-1   ← thread-1 reused after task 0 finished
// task 2 on pool-1-thread-3
// ...
      

2. Cached thread pool — newCachedThreadPool

Creates threads as needed, reuses idle threads, removes threads idle for 60 seconds. Good for many short-lived tasks; unbounded growth possible.


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

public class CachedPoolExample {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();

        pool.submit(() -> System.out.println("short task"));
        pool.shutdown();
    }
}
      

3. Custom pool — ThreadPoolExecutor

Full control over core/max pool size, queue type, and rejection policy.


import java.util.concurrent.*;

public class CustomPoolExample {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            2,                          // core pool size
            4,                          // max pool size
            60L, TimeUnit.SECONDS,      // idle thread timeout
            new ArrayBlockingQueue<>(100),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );

        pool.submit(() -> System.out.println("custom pool task"));
        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.MINUTES);
    }
}
      

4. Virtual thread pool (Java 21+)

Not an OS thread pool — each task gets a lightweight virtual thread. Same submit/wait API, far higher concurrency for I/O-bound work.


import java.util.concurrent.Executors;

public class VirtualPoolExample {
    public static void main(String[] args) {
        try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10_000; i++) {
                int id = i;
                pool.submit(() -> System.out.println("vt-task " + id));
            }
        }
    }
}