In the world of concurrent programming, efficient task execution and resource management are critical. Instead of spawning a new thread for every task, thread pools offer a smarter alternative: reuse a fixed set of threads to execute a stream of tasks efficiently.
While Java provides robust tools like ExecutorService
and ThreadPoolExecutor
, building your own custom thread pool helps you understand the internal mechanics, such as queue handling, worker management, and synchronization.
In this guide, you’ll learn to implement your own thread pool executor and understand what makes the built-in ones tick.
💡 Why Build a Custom Thread Pool?
- Fine-grained control over queueing, blocking, rejection, and worker lifecycle
- Learn how Java handles scheduling, shutdown, and worker termination
- Useful for embedded systems or learning environments
- Customize task prioritization or logging
🧩 Core Concepts
Thread Pool
A pool of pre-created worker threads that wait for tasks and execute them.
Executor
An abstraction that decouples task submission from execution.
Work Queue
A thread-safe structure that holds submitted tasks before execution.
🔁 Thread Lifecycle
- NEW → RUNNABLE → WAITING (for tasks) → TERMINATED
- Custom pools must handle thread interruption, task rejection, and graceful shutdown
🧱 Step-by-Step: Building a Custom Thread Pool
1. Define the Task Queue
class TaskQueue {
private final Queue<Runnable> queue = new LinkedList<>();
public synchronized void enqueue(Runnable task) {
queue.offer(task);
notify(); // wake up worker
}
public synchronized Runnable dequeue() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // block until task arrives
}
return queue.poll();
}
}
2. Create Worker Threads
class Worker extends Thread {
private final TaskQueue queue;
private volatile boolean running = true;
public Worker(TaskQueue queue) {
this.queue = queue;
}
public void run() {
while (running) {
try {
Runnable task = queue.dequeue();
task.run();
} catch (InterruptedException ignored) {}
}
}
public void shutdown() {
running = false;
this.interrupt();
}
}
3. Build the Thread Pool Class
class CustomThreadPool {
private final TaskQueue queue = new TaskQueue();
private final List<Worker> workers = new ArrayList<>();
public CustomThreadPool(int size) {
for (int i = 0; i < size; i++) {
Worker worker = new Worker(queue);
workers.add(worker);
worker.start();
}
}
public void submit(Runnable task) {
queue.enqueue(task);
}
public void shutdown() {
for (Worker worker : workers) {
worker.shutdown();
}
}
}
🚀 Usage Example
public class Main {
public static void main(String[] args) {
CustomThreadPool pool = new CustomThreadPool(4);
for (int i = 0; i < 10; i++) {
int taskId = i;
pool.submit(() -> {
System.out.println("Running task " + taskId);
});
}
pool.shutdown();
}
}
🛠 Enhancements for Production-Ready Executor
- Task timeout and rejection policies
- Bounded queues (e.g.,
ArrayBlockingQueue
) - Thread naming, daemon setting
- Future-like return values (custom
FutureTask
) - Monitoring thread pool status
⚙ Internal Memory and JMM Considerations
- Shared queue access → synchronized blocks or
ReentrantLock
- Worker loop uses
volatile
flag for visibility - Task submission must establish happens-before relationship with execution
📂 Comparison: Custom vs Built-in ThreadPoolExecutor
Feature | Custom Executor | ThreadPoolExecutor |
---|---|---|
Reusability | ✔️ | ✔️ |
Bounded queue support | ⚠️ (manual) | ✔️ |
Task rejection | ❌ | ✔️ |
Thread factory | ❌ | ✔️ |
Monitoring hooks | ❌ | ✔️ |
Scheduled tasks | ❌ | ✔️ (ScheduledThreadPoolExecutor ) |
📌 What's New in Java?
Java 8
- Lambdas make Runnable more concise
CompletableFuture
for async programming
Java 9
- Flow API (Reactive Streams)
Java 11
- Improvements to CompletableFuture
Java 17
- Sealed classes, record enhancements
Java 21
- ✅ Virtual Threads via Project Loom
- ✅ Structured Concurrency
- ✅ Scoped Values
Custom thread pools can be adapted to manage virtual threads using Executors.newVirtualThreadPerTaskExecutor()
.
✅ Best Practices
- Always use
finally
blocks for releasing resources - Don't use unbounded queues in production
- Avoid blocking calls in worker threads
- Tune thread count based on CPU cores (
Runtime.getRuntime().availableProcessors()
) - Avoid direct
new Thread()
calls in production
🚫 Anti-Patterns
- Swallowing exceptions inside
Runnable.run()
- Letting threads run forever without shutdown hooks
- Using polling or busy-waiting instead of
wait()/notify()
- Forgetting to handle
InterruptedException
- Ignoring synchronization on shared queues
🧰 Design Patterns
- Worker Thread – Thread pool delegates tasks to worker threads
- Thread-per-Message – One thread per message (inefficient)
- Producer-Consumer – Pool acts as consumer, clients as producers
📘 Conclusion and Key Takeaways
- Building a thread pool executor from scratch teaches concurrency fundamentals
- Helps you understand worker lifecycle, task scheduling, and queue management
- Java’s built-in
ThreadPoolExecutor
is powerful, but customization gives fine-grained control - Combine your custom executor with modern tools like virtual threads, structured concurrency, and completable futures
❓ FAQ
1. Is it recommended to write your own thread pool?
Only for educational or highly specialized purposes. Use built-in options for production.
2. What’s the role of wait()
and notify()
here?
To block and resume worker threads waiting on task queue.
3. Can I reuse threads for different tasks?
Yes, that’s the purpose of thread pooling.
4. Why not use Executors.newFixedThreadPool()
?
It’s easier and robust, but lacks full customization.
5. How can I handle shutdown gracefully?
Set a flag and interrupt all workers.
6. What happens if a task throws an exception?
Unless caught, the thread may terminate—always use try-catch in task wrappers.
7. Can I block in a worker?
Prefer not to; it ties up valuable threads. Use async I/O if needed.
8. How many threads should I use?
Depends on workload: CPU-bound (core count), IO-bound (more threads).
9. Can I return results from tasks?
Yes, by extending to support FutureTask
or Callable
.
10. Should I use virtual threads instead?
Yes, for lightweight, high-concurrency tasks in Java 21+.