Design (LLD) Thread Pool - Machine Coding

Design (LLD) Thread Pool - Machine Coding

Features Required:

  1. Thread Management:

    • Fixed number of threads.

    • Ability to execute tasks concurrently.

    • Graceful shutdown of threads.

  2. Task Queue:

    • A blocking queue to hold tasks before they are executed.

    • Ability to handle tasks when all threads are busy.

  3. Task Submission:

    • Interface for submitting tasks.

    • Support for both short and long-running tasks.

  4. Thread Reusability:

    • Reuse threads for executing multiple tasks to minimize overhead.
  5. Graceful Shutdown:

    • Ability to shutdown the thread pool gracefully, ensuring all tasks are completed.

Design Patterns Involved:

  1. Singleton Pattern:

    • Used to ensure only one instance of the thread pool is created, providing global access.
  2. Factory Method Pattern:

    • Used to create task objects and threads, encapsulating the instantiation logic.
  3. Producer-Consumer Pattern:

    • Implemented using a blocking queue where tasks are produced by the main thread and consumed by worker threads.
  4. Thread Pool Pattern:

    • Core pattern where a pool of threads is maintained to perform tasks concurrently.
  5. Strategy Pattern:

    • Can be used to manage different task scheduling algorithms (FIFO, LIFO, Priority, etc.).

Multiple DS/Algorithms Involved:

  1. Task Scheduling:

    • FIFO (First-In-First-Out): Tasks are executed in the order they are submitted.

    • Priority-based: Tasks can be prioritized based on certain criteria.

  2. Task Execution:

    • Runnable Interface: Tasks implement the Runnable interface and are executed by worker threads.
  3. Blocking Queue Implementation:

    • LinkedBlockingQueue: A thread-safe implementation of a blocking queue to manage tasks.

Code (Java):

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;

// Singleton Pattern - ThreadPool Class
public class ThreadPool {

    private static ThreadPool instance;
    private final BlockingQueue<Runnable> taskQueue;
    private final Thread[] workers;
    private final AtomicBoolean isShutDownInitiated;

    // Private Constructor for Singleton
    private ThreadPool(int numberOfThreads) {
        taskQueue = new LinkedBlockingQueue<>();
        workers = new Thread[numberOfThreads];
        isShutDownInitiated = new AtomicBoolean(false);

        for (int i = 0; i < numberOfThreads; i++) {
            workers[i] = new Worker(taskQueue, isShutDownInitiated);
            workers[i].start();
        }
    }

    // Singleton getInstance method
    public static synchronized ThreadPool getInstance(int numberOfThreads) {
        if (instance == null) {
            instance = new ThreadPool(numberOfThreads);
        }
        return instance;
    }

    // Method to submit task
    public void submitTask(Runnable task) {
        if (!isShutDownInitiated.get()) {
            taskQueue.offer(task);
        }
    }

    // Method to shutdown the pool
    public void shutdown() {
        isShutDownInitiated.set(true);
        for (Thread worker : workers) {
            worker.interrupt();
        }
    }
}

// Worker Class implementing Runnable (Factory Method Pattern)
class Worker extends Thread {

    private final BlockingQueue<Runnable> taskQueue;
    private final AtomicBoolean isShutDownInitiated;

    public Worker(BlockingQueue<Runnable> taskQueue, AtomicBoolean isShutDownInitiated) {
        this.taskQueue = taskQueue;
        this.isShutDownInitiated = isShutDownInitiated;
    }

    @Override
    public void run() {
        while (!isShutDownInitiated.get() || !taskQueue.isEmpty()) {
            try {
                Runnable task = taskQueue.take();
                task.run();
            } catch (InterruptedException e) {
                if (isShutDownInitiated.get()) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }
    }
}

// Task Class implementing Runnable (Factory Method Pattern)
class Task implements Runnable {

    private final String taskName;

    public Task(String taskName) {
        this.taskName = taskName;
    }

    @Override
    public void run() {
        System.out.println("Executing Task: " + taskName);
        try {
            Thread.sleep(1000); // Simulate task execution
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Task Completed: " + taskName);
    }
}

// Main Class to test the ThreadPool implementation
public class ThreadPoolExample {

    public static void main(String[] args) {
        ThreadPool threadPool = ThreadPool.getInstance(3);

        // Submitting tasks to ThreadPool
        for (int i = 1; i <= 10; i++) {
            Task task = new Task("Task-" + i);
            threadPool.submitTask(task);
        }

        // Shutting down the thread pool
        threadPool.shutdown();
    }
}

Explanation:

  1. Singleton Pattern:

    • The ThreadPool class follows the Singleton pattern, ensuring that only one instance of the thread pool is created and used globally.
  2. Factory Method Pattern:

    • Both Worker and Task classes are instantiated using the factory method, which encapsulates the creation logic.
  3. Producer-Consumer Pattern:

    • The LinkedBlockingQueue is used to hold tasks. Tasks are produced by the main thread and consumed by the worker threads.
  4. Thread Pool Pattern:

    • The ThreadPool class maintains a fixed pool of threads to execute tasks concurrently.
  5. Strategy Pattern (Optional):

    • If you want to extend the design, you could implement different scheduling strategies for task execution.

This design is modular and can be easily extended to incorporate additional features like task prioritization, dynamic thread pool sizing, etc.

Questions (Covered in our premium course)

  1. How can I modify this Thread Pool design to handle different types of task scheduling strategies (e.g., priority-based, round-robin, etc.)?

    • Answer: Modifying the design to handle different scheduling strategies can be done by implementing the Strategy Pattern. You can create different scheduler classes that implement a common interface, allowing the thread pool to switch between strategies at runtime. This flexibility is key in systems where tasks have varying importance or urgency.
  2. What are the best practices for scaling the thread pool dynamically based on system load?

    • Answer: Dynamic scaling of the thread pool involves monitoring system resources (CPU, memory) and adjusting the number of active threads accordingly. This can be done by periodically checking system load and adding or removing threads from the pool. Additionally, using work-stealing queues can help balance the load among threads.
  3. How would you implement a timeout mechanism for tasks in the thread pool?

    • Answered: Explore timeout strategies and their implementation in multi-threaded systems in our LLD course.
  4. What if I need to monitor and log the status of tasks being executed in the thread pool?

    • Learn about integrating logging and monitoring into your design for better observability in our LLD course.
  5. How can I handle exceptions in tasks without disrupting the entire thread pool?

    • Answer: Exception handling in a thread pool can be managed by wrapping task execution in a try-catch block. If an exception occurs, it can be logged or handled appropriately without bringing down the entire pool. Moreover, you can implement a callback mechanism to notify when a task fails, allowing the system to take corrective actions.
  6. Can this thread pool be extended to work in a distributed environment across multiple machines?

    • Our LLD course includes sections on designing distributed systems, including extending thread pools for distributed architectures.
  7. What are the memory management concerns with this implementation, and how can they be addressed?

    • Answer: Memory management concerns in a thread pool include ensuring that tasks do not hold onto resources longer than necessary, which could lead to memory leaks. Properly managing object lifecycles and using weak references for listeners or callbacks are some techniques to avoid such issues.
  8. How would you test such a thread pool implementation effectively?

    • Testing concurrent systems is tricky; our course provides detailed testing strategies for multi-threaded and concurrent applications.
  9. Are there any design patterns or architectural improvements that could make this implementation more efficient?

    • Answer: Implementing patterns like the Work Stealing pattern can improve efficiency, especially in scenarios with varying task loads. Additionally, using a Fork/Join framework for divide-and-conquer tasks can lead to more efficient utilization of threads in the pool.
  10. How does this implementation compare with Java’s built-in ExecutorService?

    • We provide in-depth comparisons between custom implementations and Java’s built-in libraries in our LLD course, helping you make informed design choices.

Did you find this article valuable?

Support Subhahu Jain by becoming a sponsor. Any amount is appreciated!