Implementing a Custom Thread Pool Using LinkedBlockingQueue in Java
Complete Step-by-Step Guide
Introduction
A Thread Pool is a collection of pre-created worker threads that execute tasks from a shared queue.
Instead of creating a new thread for every task (which is costly in terms of performance), a thread pool reuses threads, reducing overhead and improving efficiency.
Java provides ExecutorService as its thread pool implementation, but building a custom thread pool helps us understand:
- How threads can be reused.
- How tasks can be queued and processed.
- How thread lifecycle management works.
In this blog, we will build a CustomThreadPool using our previously implemented CustomLinkedBlockingQueue.
Step-by-Step Implementation
Step 1 — Understand Requirements
Our custom thread pool must:
- Maintain a fixed number of worker threads.
- Use a task queue to store incoming tasks.
- Assign tasks to available threads.
- Allow graceful shutdown of the pool.
- Handle synchronization between threads.
Step 2 — Create the Worker Thread
Each worker thread repeatedly fetches tasks from the queue and executes them.
class Worker extends Thread {
private final CustomLinkedBlockingQueue taskQueue;
private volatile boolean isRunning = true;
public Worker(CustomLinkedBlockingQueue taskQueue) {
this.taskQueue = taskQueue;
}
@Override
public void run() {
while (isRunning) {
try {
Runnable task = taskQueue.take();
task.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
public void shutdown() {
isRunning = false;
this.interrupt();
}
}
Step 3 — Create the CustomThreadPool Class
This class manages workers and a shared queue for tasks.
public class CustomThreadPool {
private final CustomLinkedBlockingQueue taskQueue;
private final Worker[] workers;
public CustomThreadPool(int poolSize, int queueCapacity) {
taskQueue = new CustomLinkedBlockingQueue<>(queueCapacity);
workers = new Worker[poolSize];
for (int i = 0; i < poolSize; i++) {
workers[i] = new Worker(taskQueue);
workers[i].start();
}
}
public void execute(Runnable task) throws InterruptedException {
taskQueue.put(task);
}
public void shutdown() {
for (Worker worker : workers) {
worker.shutdown();
}
}
}
Step 4 — Test the CustomThreadPool
We will test by submitting tasks and letting worker threads process them.
public class ThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
CustomThreadPool threadPool = new CustomThreadPool(3, 5);
for (int i = 1; i <= 10; i++) {
final int taskNumber = i;
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " executing task " + taskNumber);
try {
Thread.sleep(1000); // simulate work
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
Thread.sleep(8000); // wait for tasks to complete
threadPool.shutdown();
}
}
Step 5 — Expected Output
Thread-0 executing task 1
Thread-1 executing task 2
Thread-2 executing task 3
Thread-0 executing task 4
Thread-1 executing task 5
...
Explanation:
- Worker threads continuously pull tasks from the queue.
- Tasks are processed in FIFO order.
- Threads are reused instead of being created anew for every task.
Step 6 — Add Graceful Shutdown
To ensure that threads stop after completing tasks, we modify the shutdown method:
public void shutdownGracefully() {
for (Worker worker : workers) {
worker.shutdown();
}
}
Worker threads exit when isRunning becomes false.
Complete code
// Node class for the queue
class Node {
T item;
Node next;
Node(T item) {
this.item = item;
this.next = null;
}
}
// Custom LinkedBlockingQueue implementation
class CustomLinkedBlockingQueue {
private Node head;
private Node tail;
private int size;
private final int capacity;
public CustomLinkedBlockingQueue(int capacity) {
this.capacity = capacity;
head = tail = new Node<>(null); // dummy node
}
// Add task to queue (blocks if full)
public synchronized void put(T item) throws InterruptedException {
while (size == capacity) {
wait();
}
Node newNode = new Node<>(item);
tail.next = newNode;
tail = newNode;
size++;
notifyAll(); // notify waiting threads
}
// Take task from queue (blocks if empty)
public synchronized T take() throws InterruptedException {
while (size == 0) {
wait();
}
Node first = head.next;
head.next = first.next;
size--;
if (size == 0) {
tail = head; // reset tail
}
notifyAll(); // notify producers
return first.item;
}
public synchronized int size() {
return size;
}
}
// Worker thread that executes tasks
class Worker extends Thread {
private final CustomLinkedBlockingQueue taskQueue;
private volatile boolean isRunning = true;
public Worker(CustomLinkedBlockingQueue taskQueue) {
this.taskQueue = taskQueue;
}
@Override
public void run() {
while (isRunning) {
try {
Runnable task = taskQueue.take();
task.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
public void shutdown() {
isRunning = false;
this.interrupt();
}
}
// Custom Thread Pool implementation
class CustomThreadPool {
private final CustomLinkedBlockingQueue taskQueue;
private final Worker[] workers;
public CustomThreadPool(int poolSize, int queueCapacity) {
taskQueue = new CustomLinkedBlockingQueue<>(queueCapacity);
workers = new Worker[poolSize];
for (int i = 0; i < poolSize; i++) {
workers[i] = new Worker(taskQueue);
workers[i].start();
}
}
public void execute(Runnable task) throws InterruptedException {
taskQueue.put(task);
}
public void shutdown() {
for (Worker worker : workers) {
worker.shutdown();
}
}
public void shutdownGracefully() {
shutdown();
}
}
// Test class
public class ThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
CustomThreadPool threadPool = new CustomThreadPool(3, 5);
for (int i = 1; i <= 10; i++) {
final int taskNumber = i;
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " executing task " + taskNumber);
try {
Thread.sleep(1000); // simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
Thread.sleep(8000); // wait for some tasks to complete
threadPool.shutdownGracefully();
}
}
✅ How it works:
- CustomLinkedBlockingQueue handles task storage safely using wait() and notifyAll().
- Worker threads continuously take tasks from the queue and execute them.
- CustomThreadPool manages worker lifecycle and task submission.
- The main test submits 10 tasks to a pool of 3 threads, demonstrating thread reuse and queue-based execution.
Real-World Use Case
Custom thread pools are useful for:
- Web servers processing multiple requests.
- Batch processing systems.
- Applications with predictable, recurring tasks.
- Background job execution.
Advantages
- Performance improvement: Thread reuse reduces creation overhead.
- Resource management: Controls the number of concurrent threads.
- Task scheduling: Centralized queue allows orderly task execution.
Disadvantages
- Implementation complexity: More code to maintain compared to built-in thread pools.
- Synchronization overhead: Managing threads and queues requires careful synchronization.
- Shutdown handling: Requires explicit management to stop threads gracefully.
Summary
The custom thread pool is an essential multithreading concept that shows how threads can be reused to efficiently handle multiple tasks.
By implementing it step-by-step, we learned how to:
- Maintain a fixed number of worker threads.
- Use a shared blocking queue to manage tasks.
- Synchronize threads to prevent race conditions.
- Gracefully shut down the pool.
This approach significantly improves application performance by avoiding the cost of creating and destroying threads repeatedly. The custom thread pool also demonstrates how to coordinate worker threads and tasks effectively.
Although Java provides the ExecutorService and ThreadPoolExecutor classes as robust thread pool implementations, creating a custom thread pool builds a deeper understanding of thread management, task queuing, and blocking behavior. Such knowledge is critical for designing high-performance multithreaded systems and forms the foundation for advanced concurrency programming.
