Leader-Followers Pattern in Java: Enhancing Efficiency with Dynamic Worker Allocation
Intent of Leader/Followers Design Pattern
To efficiently manage a set of worker threads, the Leader-Followers pattern enables multiple threads to take turns sharing a set of event sources, optimizing resource utilization and improving performance compared to a one-thread-per-source approach.
Detailed Explanation of Leader/Followers Pattern with Real-World Examples
Real-world Example
Imagine managing a busy restaurant with multiple servers and a single host. The host acts as the "leader" and is responsible for greeting guests, managing the waitlist, and seating guests. Once the guests are seated, the host returns to the entrance to manage new arrivals. The servers, or "followers," wait for the host to assign them tables. This assignment is done on a rotational basis where the next available server takes the next group of guests. This system ensures that the host efficiently handles the incoming flow of guests while servers focus on providing service, similar to how the Leader and Followers pattern manages threads and tasks in a software system. This approach optimizes resource utilization (in this case, staff) and ensures smooth operations during peak times, much like it optimizes thread usage in computing environments.
In plain words
The Leader-Followers design pattern utilizes a single leader thread to distribute work among multiple follower threads, effectively managing task delegation, thread synchronization, and improving resource efficiency in concurrent programming.
martinfowler.com says
Select one server in the cluster as a leader. The leader is responsible for taking decisions on behalf of the entire cluster and propagating the decisions to all the other servers.
Programmatic Example of Leader-Followers Pattern in Java
The Leader-Followers pattern is a concurrency design pattern where one thread (the leader) waits for work to arrive, de-multiplexes, dispatches, and processes the work, thereby enhancing CPU cache affinity and reducing event dispatching latency. Once the leader finishes processing the work, it promotes one of the follower threads to be the new leader. This pattern is useful for enhancing CPU cache affinity, minimizing locking overhead, and reducing event dispatching latency.
In the provided code, we have a WorkCenter
class that manages a group of Worker
threads. One of these workers is designated as the leader and is responsible for receiving and processing tasks. Once a task is processed, the leader promotes a new leader from the remaining workers.
// WorkCenter class
public class WorkCenter {
@Getter
private Worker leader;
private final List<Worker> workers = new CopyOnWriteArrayList<>();
// Method to create workers and set the initial leader
public void createWorkers(int numberOfWorkers, TaskSet taskSet, TaskHandler taskHandler) {
for (var id = 1; id <= numberOfWorkers; id++) {
var worker = new Worker(id, this, taskSet, taskHandler);
workers.add(worker);
}
promoteLeader();
}
// Method to promote a new leader
public void promoteLeader() {
Worker leader = null;
if (!workers.isEmpty()) {
leader = workers.get(0);
}
this.leader = leader;
}
}
In the Worker
class, each worker is a thread that waits for tasks to process. If the worker is the leader, it processes the task and then promotes a new leader.
// Worker class
public class Worker implements Runnable {
private final long id;
private final WorkCenter workCenter;
private final TaskSet taskSet;
private final TaskHandler taskHandler;
@Override
public void run() {
while (!Thread.interrupted()) {
try {
if (workCenter.getLeader() != null && !workCenter.getLeader().equals(this)) {
synchronized (workCenter) {
if (workCenter.getLeader() != null && !workCenter.getLeader().equals(this)) {
workCenter.wait();
continue;
}
}
}
final Task task = taskSet.getTask();
synchronized (workCenter) {
workCenter.removeWorker(this);
workCenter.promoteLeader();
workCenter.notifyAll();
}
taskHandler.handleTask(task);
workCenter.addWorker(this);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
}
In the App
class, we create a WorkCenter
, add tasks to a TaskSet
, and then start the workers. The leader worker will start processing the tasks, and once it finishes a task, it will promote a new leader.
// App class
public class App {
public static void main(String[] args) throws InterruptedException {
var taskSet = new TaskSet();
var taskHandler = new TaskHandler();
var workCenter = new WorkCenter();
workCenter.createWorkers(4, taskSet, taskHandler);
addTasks(taskSet);
startWorkers(workCenter);
}
private static void addTasks(TaskSet taskSet) throws InterruptedException {
var rand = new SecureRandom();
for (var i = 0; i < 5; i++) {
var time = Math.abs(rand.nextInt(1000));
taskSet.addTask(new Task(time));
}
}
private static void startWorkers(WorkCenter workCenter) throws InterruptedException {
var workers = workCenter.getWorkers();
var exec = Executors.newFixedThreadPool(workers.size());
workers.forEach(exec::submit);
exec.awaitTermination(2, TimeUnit.SECONDS);
exec.shutdownNow();
}
}
This is a basic example of the Leader/Followers pattern. The leader worker processes tasks and promotes a new leader once it finishes a task. The new leader then starts processing the next task, and the cycle continues.
When to Use the Leader/Followers Pattern in Java
- The Leader-Followers pattern is useful in scenarios requiring efficient handling of multiple services on a single thread, avoiding resource thrashing, and improving scalability in concurrent programming environments.
- Applicable in server environments where multiple client requests must be handled concurrently with minimal resource consumption.
Real-World Applications of Leader-Followers Pattern in Java
- Network servers handling multiple incoming connections.
- Event-driven applications that manage a large number of input/output sources.
Benefits and Trade-offs of Leader/Followers Pattern
Benefits:
- Reduces the number of threads and context switching, leading to better performance and lower resource utilization.
- Improves system scalability and responsiveness.
Trade-offs:
- Increased complexity in managing the synchronization between leader and followers.
- Potential for underutilization of resources if not correctly implemented.
Related Java Design Patterns
- Half-Sync/Half-Async: Leader and Followers can be seen as a variation where the synchronization aspect is divided between the leader (synchronous handling) and followers (waiting asynchronously).
- Thread Pool: Both patterns manage a pool of worker threads, but Thread Pool assigns tasks to any available thread rather than using a leader to distribute work.