0% found this document useful (0 votes)
0 views

Java Multithreading for Senior Engineering Interviews Part II

The document discusses Java multithreading concepts essential for senior engineering interviews, focusing on memory models, happens-before relationships, and synchronization techniques. It explains how Java's memory model (JMM) ensures thread safety through various rules, such as program order, monitor locks, volatile variables, thread start and join, and thread interruption. Additionally, it covers the implementation of blocking queues to manage producer-consumer scenarios, emphasizing their thread-safe characteristics and internal synchronization mechanisms.

Uploaded by

Anshul Shah
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
0 views

Java Multithreading for Senior Engineering Interviews Part II

The document discusses Java multithreading concepts essential for senior engineering interviews, focusing on memory models, happens-before relationships, and synchronization techniques. It explains how Java's memory model (JMM) ensures thread safety through various rules, such as program order, monitor locks, volatile variables, thread start and join, and thread interruption. Additionally, it covers the implementation of blocking queues to manage producer-consumer scenarios, emphasizing their thread-safe characteristics and internal synchronization mechanisms.

Uploaded by

Anshul Shah
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 77

archive.today Saved from https://medium.

com/@yugalnandurkar5/java-multithreading-for-senior-engine search 30 Mar 2025 01:29:28 UTC


webpage capture no other snapshots from this url
All snapshots from host medium.com

Webpage Screenshot share download .zip report bug or abuse Buy me a coffee

Search Write Sign up Sign in

Java Multithreading for Senior


Engineering Interviews (Part II)
yugal-nandurkar · Follow
59 min read · Jan 12, 2025

Java Multithreading for Senior Engineering Interviews (Part I) | by yugal-


nandurkar | Jan, 2025 | Medium

Reordering Effects
Different processor architectures have different policies as to when an
individual processor’s cache is reconciled with the main memory.

For instance, the frequency of reconciling a processor’s cache with the main
memory depends on the processor architecture. A processor may relax its
memory coherence guarantees in favor of better performance. The
architecture’s memory model specifies the guarantees a program can expect
from the memory model. It will also specify instructions required to get
additional memory coordination guarantees when data is being shared
among threads. These instructions are usually called memory fences or
barriers but the Java developer can rely on the JVM to interface with the
underlying platform’s memory model through its own memory model called
JMM (Java Memory Model) and insert these platform memory specific
instructions appropriately. Conversely, the JVM relies on the developer to
identify when data is shared through the use of proper synchronization.

[Processor Cache State]


|-> Regular
|-> Relaxed

[Processor Memory Model]


|-> Standard Guarantees
|-> Additional Guarantees

[JVM Memory Model (JMM)]


|-> Standard Interface
|-> Insert Platform-Specific Instructions

[Java Developer]
|-> Proper Synchronization Used
|-> Improper Synchronization

The happens-before Relationship


The JMM defines a partial ordering on all actions within a program.

The sequence of natural numbers i.e. 1, 2, 3 4, …. is a total ordering. Each


element is either greater or smaller than any other element in the set of
natural numbers (Totality). If 2 < 4 and 4 < 7 then we know that 2 < 7
necessarily (Transitivity). And finally if 3 < 5 then 5 can’t be less than 3
(Asymmetry).

Elements of a set will exhibit partial ordering when they possess transitivity
and asymmetry but not totality. As an example think about your family tree.
Your father is your ancestor, your grandfather is your father’s ancestor. By
transitivity, your grandfather is also your ancestor. However, your father or
grandfather aren’t ancestors of your mother and in a sense they are
incomparable.

The compiler in the spirit of optimization is free to reorder statements


however it must make sure that the outcome of the program is the same as
without reordering. The sources of reordering can be numerous. Some
examples include: If two fields X and Y are being assigned but don’t depend
on each other, then the compiler is free to reorder them Processors may
execute instructions out of order under some circumstances Data may be
juggled around in the registers, processor cache or the main memory in an
order not specified by the program e.g. Y can be flushed to main memory
before X.

The JMM is defined in terms of actions which can be any of the following,
read and writes of variables, locks and unlocks of monitors, starting and
joining of threads. The JMM enforces a happens-before ordering on these
actions. When an action A happens-before an action B, it implies that A is
guaranteed to be ordered before B and visible to B. The reordering tricks are
harmless in case of a single threaded program but all hell will break loose
when we introduce another thread that shares the data that is being read or
written to in the writerThread method.

A happens before relationship can be established in the following ways;


Each action in a thread happens-before every action in that thread that
comes later in the program’s order. However, for a single threaded program,
instructions can be reordered but the semantics of the program order is still
preserved. An unlock on a monitor happens-before every subsequent lock
on that same monitor. The synchronization block is equivalent of a monitor.
A write to a volatile field happens-before every subsequent read of that same
volatile. A call to start() on a thread happens-before any actions in the started
thread. All actions in a thread happen-before any other thread successfully
returns from a join() on that thread. The constructor for an object happens-
before the start of the finalizer for that object A thread interrupting another
thread happens-before the interrupted thread detects it has been
interrupted.

In Java’s concurrency model, the happens-before relationship defines the


order in which operations occur, ensuring memory visibility and
consistency across threads. Establishing happens-before relationships is
crucial for writing thread-safe programs. Let’s explore how these
relationships are established through various scenarios, accompanied by
detailed Java code examples.

1. Program Order Rule

Within a single thread, each action happens-before every subsequent action


in that thread. This means that statements are executed in the order they
appear in the program, ensuring predictable behavior within the thread.

public class ProgramOrderExample {


public static void main(String[] args) {
int a = 1; // Action 1
int b = a + 1; // Action 2
System.out.println(b); // Action 3
}
}

In this example, Action 1 happens-before Action 2, and Action 2 happens-


before Action 3 within the same thread.

2. Monitor Lock Rule


An unlock on a monitor (synchronized block) happens-before every
subsequent lock on that same monitor. This ensures that changes made by
one thread within a synchronized block are visible to another thread that
subsequently enters a synchronized block protected by the same monitor.

public class MonitorLockExample {


private int sharedData = 0;

public synchronized void writerThread() {


sharedData = 42; // Write to shared data
}

public synchronized void readerThread() {


System.out.println(sharedData); // Read shared data
}
}

Here, the writerThread method's unlock happens-before the readerThread

method's lock on the same monitor, ensuring visibility of sharedData .

3. Volatile Variable Rule

A write to a volatile field happens-before every subsequent read of that


same volatile field. This guarantees that a thread reading a volatile

variable sees the most recent write to that variable.

public class VolatileExample {


private volatile boolean flag = false;

public void writerThread() {


flag = true; // Write to volatile variable
}

public void readerThread() {


if (flag) {
System.out.println("Flag is true");
}
}
}

In this case, the write to flag in writerThread happens-before the read of


flag in readerThread , ensuring visibility.

4. Thread Start Rule

A call to start() on a thread happens-before any actions in the started


thread. This ensures that the new thread sees the effects of all memory
writes made by the thread that started it.

public class ThreadStartExample {


private int sharedData = 0;

public void mainThread() {


sharedData = 100; // Write to shared data

Thread newThread = new Thread(() -> {


System.out.println(sharedData); // Read shared data
});

newThread.start(); // Start the new thread


}
}

Here, the mainThread 's actions happen-before any actions in newThread ,

ensuring newThread sees the updated sharedData .

5. Thread Join Rule

All actions in a thread happen-before any other thread successfully returns


from a join() on that thread. This ensures that after a thread has finished
execution, other threads waiting for it using join() see the effects of its
actions.

public class ThreadJoinExample {


private int sharedData = 0;

public void mainThread() throws InterruptedException {


Thread workerThread = new Thread(() -> {
sharedData = 200; // Write to shared data
});

workerThread.start();
workerThread.join(); // Wait for workerThread to finish

System.out.println(sharedData); // Read shared data


}
}

In this example, all actions in workerThread happen-before the mainThread

returns from join() , ensuring visibility of sharedData .

6. Finalizer Rule

The constructor for an object happens-before the start of the finalizer for
that object. This ensures that an object’s finalizer sees the fully constructed
state of the object.

public class FinalizerExample {


private int data;

public FinalizerExample(int data) {


this.data = data; // Constructor writes to data
}

@Override
protected void finalize() throws Throwable {
System.out.println(data); // Finalizer reads data
super.finalize();
}
}

Here, the constructor’s actions happen-before the finalize() method,


ensuring it sees the initialized data .

7. Thread Interruption Rule

A thread interrupting another thread happens-before the interrupted thread


detects it has been interrupted. This ensures that when a thread interrupts
another, the interrupted thread reliably detects the interrupt.

public class ThreadInterruptionExample {


public void mainThread() {
Thread workerThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Perform task
}
System.out.println("Thread was interrupted");
});

workerThread.start();
workerThread.interrupt(); // Interrupt the worker thread
}
}

In this scenario, the call to interrupt() happens-before workerThread detects


the interruption, ensuring proper handling.

Understanding and applying these happens-before relationships are


essential for developing correct and thread-safe concurrent applications in
Java. They provide a framework for reasoning about memory visibility and
synchronization, preventing subtle concurrency bugs.
For a more in-depth understanding, you can refer to the Java Language
Specification, Chapter 17: Threads and Locks.

This implies that any memory operations which were visible to a thread
before exiting a synchronized block are visible to any thread after it enters a
synchronized block protected by the same monitor, since all the memory
operations happen before the release, and the release happens before the
acquire. Exiting a synchronized block causes the cache to be flushed to the
main memory so that the writes made by the exiting thread are visible to
other threads. Similarly, entering a synchronized block has the effect of
invalidating the local processor cache and reloading of variables from the
main memory so that the entering thread is able to see the latest values.

All it means is that when readerThread releases the monitor, up till that
point, whatever shared variables it has manipulated will have their latest
values visible to the writerThread as soon as it acquires the same monitor. If
it acquires a different monitor then there’s no happens-before relationship
and it may or may not see the latest values for the shared variables.

Blocking Queue | Bounded Buffer | Consumer Producer


Classical synchronization problem involving a limited size buffer which can
have items added to it or removed from it by different producer and
consumer threads. This problem is known by different names: consumer
producer problem, bounded buffer problem or blocking queue problem.

A blocking queue is defined as a queue which blocks the caller of the


enqueue method if there’s no more capacity to add the new item being
enqueued. Similarly, the queue blocks the dequeue caller if there are no
items in the queue. Also, the queue notifies a blocked enqueuing thread
when space becomes available and a blocked dequeuing thread when an
item becomes available in the queue.

In Java, a blocking queue is a thread-safe data structure that blocks attempts


to add elements when the queue is full and blocks attempts to remove
elements when the queue is empty. This behavior is particularly useful in
producer-consumer scenarios, where producers add items to the queue and
consumers remove items from it. The java.util.concurrent package
provides the BlockingQueue interface, which includes implementations like
ArrayBlockingQueue and LinkedBlockingQueue .

Key Characteristics of Blocking Queues:


Blocking on Insertion: If the queue has reached its capacity, any thread
attempting to add an element will be blocked until space becomes
available.

Blocking on Removal: If the queue is empty, any thread attempting to


remove an element will be blocked until an item is added.

Thread Safety: Blocking queues handle the necessary synchronization


internally, making them safe for use in multi-threaded environments.

Example: Producer-Consumer Using BlockingQueue

Below is a detailed example demonstrating a producer-consumer scenario


using BlockingQueue :

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class BlockingQueueExample {

private static final int QUEUE_CAPACITY = 5;


private static final int PRODUCE_COUNT = 10;
private static final int CONSUME_COUNT = 10;

public static void main(String[] args) {


BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);

Thread producerThread = new Thread(new Producer(queue, PRODUCE_COUNT));


Thread consumerThread = new Thread(new Consumer(queue, CONSUME_COUNT));

producerThread.start();
consumerThread.start();
}
}

class Producer implements Runnable {


private final BlockingQueue<Integer> queue;
private final int produceCount;

public Producer(BlockingQueue<Integer> queue, int produceCount) {


this.queue = queue;
this.produceCount = produceCount;
}

@Override
public void run() {
try {
for (int i = 1; i <= produceCount; i++) {
queue.put(i);
System.out.println("Produced: " + i);
Thread.sleep(100); // Simulate time taken to produce an item
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

class Consumer implements Runnable {


private final BlockingQueue<Integer> queue;
private final int consumeCount;
public Consumer(BlockingQueue<Integer> queue, int consumeCount) {
this.queue = queue;
this.consumeCount = consumeCount;
}

@Override
public void run() {
try {
for (int i = 1; i <= consumeCount; i++) {
Integer item = queue.take();
System.out.println("Consumed: " + item);
Thread.sleep(150); // Simulate time taken to process an item
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

BlockingQueue Initialization: An ArrayBlockingQueue with a capacity of 5


is created. This means the queue can hold up to 5 elements at a time.

Producer Thread: The producer thread adds a specified number of items


to the queue. If the queue is full, the put() method will block until space
becomes available.

Consumer Thread: The consumer thread removes a specified number of


items from the queue. If the queue is empty, the take() method will
block until an item is available.

Thread Sleep: Thread.sleep() calls are used to simulate the time taken to
produce and consume items, allowing observation of the blocking
behavior.

Key Methods of BlockingQueue :

put(E e) : Adds the specified element to the queue, waiting if necessary


for space to become available.

take() : Retrieves and removes the head of the queue, waiting if


necessary until an element becomes available.

offer(E e, long timeout, TimeUnit unit) : Adds the specified element to


the queue, waiting up to the specified wait time for space to become
available.

poll(long timeout, TimeUnit unit) : Retrieves and removes the head of


the queue, waiting up to the specified wait time if necessary for an
element to become available.

Considerations:
Thread Interruption: Both put() and take() methods can throw
InterruptedException if the thread is interrupted while waiting. Proper
handling of this exception is essential to ensure thread termination or
other interruption policies are respected.

Fairness: Some implementations, like ArrayBlockingQueue , can be


configured with fairness policies to ensure that threads are served in the
order they requested access. This is useful in scenarios where predictable
ordering of thread access is required.

Blocking queues are a powerful tool for coordinating work between multiple
threads, simplifying the implementation of producer-consumer patterns by
handling synchronization internally.

For a visual explanation and further insights into BlockingQueue in Java, you
might find the following video helpful:

Java BlockingQueue
Share

Watch on

Java Blocking Queue

Let’s see how the implementation would look like, if we were restricted to
using a mutex. There’s no direct equivalent of a theoretical mutex in Java as
each object has an implicit monitor associated with it. For this question,
we’ll use an object of the Lock class and pretend it doesn’t expose the wait()
and notify() methods and only provides mutual exclusion similar to a
theoretical mutex. Without the ability to wait or signal the implication is, a
blocked thread will constantly poll in a loop for a predicate/condition to
become true before making progress. This is an example of a busy-wait
solution.

In Java, while the Lock interface provides explicit lock mechanisms, it


doesn't directly support condition variables like wait() and notify() .
Implementing a producer-consumer scenario without these condition
variables necessitates a busy-wait approach, where threads repeatedly check
a condition in a loop until it's met. This method is generally less efficient due
to increased CPU usage but can be implemented as follows:

Busy-Wait Producer-Consumer Example Using Lock

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BusyWaitProducerConsumer {


private static final int CAPACITY = 5;
private final Queue<Integer> queue = new LinkedList<>();
private final Lock lock = new ReentrantLock();

public static void main(String[] args) {


BusyWaitProducerConsumer pc = new BusyWaitProducerConsumer();
Thread producer = new Thread(pc.new Producer());
Thread consumer = new Thread(pc.new Consumer());

producer.start();
consumer.start();
}

class Producer implements Runnable {


@Override
public void run() {
int value = 0;
while (true) {
lock.lock();
try {
while (queue.size() == CAPACITY) {
// Busy-wait until there's space in the queue
lock.unlock();
Thread.yield(); // Allow other threads to proceed
lock.lock();
}
queue.add(value);
System.out.println("Produced " + value);
value++;
} finally {
lock.unlock();
}
}
}
}

class Consumer implements Runnable {


@Override
public void run() {
while (true) {
lock.lock();
try {
while (queue.isEmpty()) {
// Busy-wait until there's an item in the queue
lock.unlock();
Thread.yield(); // Allow other threads to proceed
lock.lock();
}
int value = queue.poll();
System.out.println("Consumed " + value);
} finally {
lock.unlock();
}
}
}
}
}

Shared Queue: A LinkedList is used as the shared queue with a fixed


capacity.

Lock Mechanism: A ReentrantLock ensures mutual exclusion for accessing


the shared queue.

Producer Thread:

Acquires the lock before accessing the queue.

If the queue is full, it releases the lock, yields the processor to allow other
threads to execute, and then reacquires the lock to check the condition
again. This loop continues until space becomes available.

Once space is available, it adds an item to the queue and releases the
lock.

Consumer Thread:

Acquires the lock before accessing the queue.

If the queue is empty, it releases the lock, yields the processor, and then
reacquires the lock to check the condition again. This loop continues
until an item is available.

Once an item is available, it removes the item from the queue and
releases the lock.

Considerations:

Busy-Waiting: This approach leads to high CPU usage because threads


continuously check the condition without blocking. It’s generally
inefficient and should be avoided in favor of proper synchronization
mechanisms like wait() and notify() .

Thread.yield(): The Thread.yield() method hints to the thread scheduler


that the current thread is willing to yield its current use of a processor.
However, its effectiveness is platform-dependent and doesn't guarantee
significant performance improvement.
ReentrantLock: While ReentrantLock provides explicit lock mechanisms,
it also offers newCondition() for condition variables, which can be used
with await() and signal() methods. In this example, we avoid using
these to simulate a scenario without condition variables.

Alternative Approach:

A more efficient and recommended approach is to use BlockingQueue , which


handles synchronization internally and avoids busy-waiting. Here's how you
can implement the producer-consumer problem using BlockingQueue :

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class BlockingQueueProducerConsumer {


private static final int CAPACITY = 5;
private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(CAPACI

public static void main(String[] args) {


BlockingQueueProducerConsumer pc = new BlockingQueueProducerConsumer();
Thread producer = new Thread(pc.new Producer());
Thread consumer = new Thread(pc.new Consumer());

producer.start();
consumer.start();
}

class Producer implements Runnable {


@Override
public void run() {
int value = 0;
while (true) {
try {
queue.put(value);
System.out.println("Produced " + value);
value++;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}

class Consumer implements Runnable {


@Override
public void run() {
while (true) {
try {
int value = queue.take();
System.out.println("Consumed " + value);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
}
Advantages of Using BlockingQueue :

Built-in Blocking: Automatically blocks when the queue is full (for


producers) or empty (for consumers), eliminating the need for explicit
locking and busy-waiting.

Thread Safety: Manages synchronization internally, reducing the risk of


concurrency issues.

Simplicity: Simplifies the implementation of producer-consumer


patterns by handling the complexities of thread coordination.

In conclusion, while it’s possible to implement a producer-consumer


scenario using a busy-wait approach with explicit locks, it’s inefficient and
not recommended. Utilizing higher-level concurrency utilities like
BlockingQueue provides a more efficient and cleaner solution.

For a deeper understanding of busy-waiting and its implications, you can


refer to Busy Waiting in Operating System — Javatpoint.

As an exercise, we reproduce the two enqueue() and dequeue() methods,


without locking the mutex object when checking for the while-loop
conditions. If you run the code in the widget below multiple times, some of
the runs would display a dequeue value of null. We set an array index to null
whenever we remove its content to indicate the index is now empty. A race
condition is introduced when we check for while-loop predicate without
holding a mutex.

In concurrent programming, race conditions occur when multiple threads


access shared resources simultaneously without proper synchronization,
leading to unpredictable results. In Java, failing to use synchronization
mechanisms like mutexes (locks) when accessing shared data structures can
introduce such race conditions.

Scenario:

Consider a shared queue with enqueue() and dequeue() methods. If these


methods check their conditions (e.g., whether the queue is full or empty)
without holding a mutex, multiple threads might simultaneously find the
queue in a state that allows them to proceed, leading to inconsistent states or
errors.

Example without Proper Synchronization:


import java.util.LinkedList;
import java.util.Queue;

public class UnsafeQueue<T> {


private final Queue<T> queue = new LinkedList<>();
private final int capacity;

public UnsafeQueue(int capacity) {


this.capacity = capacity;
}

public void enqueue(T item) {


// Check condition without locking
while (queue.size() == capacity) {
// Busy-wait until there's space
}
// Proceed to add item
queue.add(item);
System.out.println("Enqueued: " + item);
}

public T dequeue() {
// Check condition without locking
while (queue.isEmpty()) {
// Busy-wait until there's an item
}
// Proceed to remove item
T item = queue.poll();
System.out.println("Dequeued: " + item);
return item;
}
}

Issues in the Above Code:

Race Conditions: Without synchronization, multiple threads might


simultaneously pass the condition checks, leading to scenarios where:

Two threads see the queue as not full and both proceed to enqueue,
potentially exceeding the capacity.

Two threads see the queue as not empty and both proceed to dequeue,
leading to null returns or errors.

Busy-Waiting: The while loops cause threads to consume CPU cycles


while waiting, leading to inefficient resource utilization.

Proper Synchronization with Mutex (Lock):

To prevent race conditions, we should use synchronization mechanisms to


ensure that only one thread can execute critical sections of code at a time. In
Java, this can be achieved using the synchronized keyword or explicit locks.

Example with Proper Synchronization:


import java.util.LinkedList;
import java.util.Queue;

public class SafeQueue<T> {


private final Queue<T> queue = new LinkedList<>();
private final int capacity;

public SafeQueue(int capacity) {


this.capacity = capacity;
}

public synchronized void enqueue(T item) throws InterruptedException {


while (queue.size() == capacity) {
wait(); // Wait until there's space
}
queue.add(item);
System.out.println("Enqueued: " + item);
notifyAll(); // Notify waiting threads
}

public synchronized T dequeue() throws InterruptedException {


while (queue.isEmpty()) {
wait(); // Wait until there's an item
}
T item = queue.poll();
System.out.println("Dequeued: " + item);
notifyAll(); // Notify waiting threads
return item;
}
}

synchronized Methods: Ensure that only one thread can execute either
enqueue() or dequeue() at a time, preventing race conditions.

wait() and notifyAll() : Manage thread coordination without busy-


waiting:

wait() causes the current thread to release the lock and wait until
notified.

notifyAll() wakes up all waiting threads, allowing them to recheck


conditions.

Conclusion:

Proper synchronization is crucial in concurrent programming to prevent


race conditions and ensure thread safety. By using synchronization
mechanisms like synchronized methods and wait() / notifyAll() , we can
coordinate thread access to shared resources effectively.

For a deeper understanding of race conditions and synchronization in Java,


you can refer to Race Condition in Java — Javatpoint.
Using Semaphores for Producer-Consumer Problem
We can also implement the bounded buffer problem using a semaphore. For
this problem, we’ll use an instance of the CountingSemaphore that we
implement in one of the later problems. A CountingSemaphore is initialized
with a maximum number of permits to give out. A thread is blocked when it
attempts to release the semaphore when none of the permits have been
given out. Similarly, a thread blocks when attempting to acquire a
semaphore that has all the permits given out. In contrast, Java’s
implementation of Semaphore can be signaled (released) even if none of the
permits, the Java semaphore was initialized with, have been used. Java’s
semaphore has no upper bound and can be released as many times as
desired to increase the number of permits.

Implementing a bounded buffer using semaphores is a classic approach to


solving the producer-consumer problem in concurrent programming. In
this scenario, producers generate data and add it to a fixed-size buffer, while
consumers remove and process data from the buffer. Proper
synchronization ensures that producers don’t add data to a full buffer and
consumers don’t remove data from an empty buffer.

Key Concepts:

Semaphores: Semaphores are synchronization tools that control access


to shared resources by maintaining a set of permits. A thread must
acquire a permit before accessing the resource and release it afterward.
In Java, the Semaphore class from the java.util.concurrent package is
commonly used for this purpose. Oracle Documentation

CountingSemaphore Class: While Java provides the Semaphore class, for


educational purposes, we can implement a custom CountingSemaphore

that allows setting the maximum number of permits and the number of
permits already given out.

Implementation Steps:

1. Define the CountingSemaphore Class:

2. We’ll create a CountingSemaphore class with methods to acquire and


release permits. The constructor will allow setting the maximum permits
and the initial number of permits already given out.
public class CountingSemaphore {
private int maxPermits;
private int usedPermits = 0;

public CountingSemaphore(int maxPermits, int initialPermits) {


this.maxPermits = maxPermits;
this.usedPermits = initialPermits;
}

public synchronized void acquire() throws InterruptedException {


while (usedPermits == maxPermits) {
wait();
}
usedPermits++;
notifyAll();
}

public synchronized void release() {


if (usedPermits > 0) {
usedPermits--;
notifyAll();
}
}
}

Initialize Semaphores for Producer and Consumer:

Producer Semaphore ( semProducer ): Initialized with a maximum number


of permits equal to the buffer size and all permits available. This allows
producer threads to add items to the buffer until it's full.

Consumer Semaphore ( semConsumer ): Initialized with a maximum


number of permits equal to the buffer size but with all permits initially
given out. This ensures that consumer threads block on dequeue() calls
when the buffer is empty.

int bufferSize = 5;
CountingSemaphore semProducer = new CountingSemaphore(bufferSize, 0);
CountingSemaphore semConsumer = new CountingSemaphore(bufferSize, bufferSize);

Implement the Bounded Buffer:

We’ll use a fixed-size queue to represent the buffer and synchronize access
to it using the semaphores.

import java.util.LinkedList;
import java.util.Queue;

public class BoundedBuffer<T> {


private Queue<T> buffer;
private int maxSize;
private CountingSemaphore semProducer;
private CountingSemaphore semConsumer;

public BoundedBuffer(int size) {


this.maxSize = size;
this.buffer = new LinkedList<>();
this.semProducer = new CountingSemaphore(size, 0);
this.semConsumer = new CountingSemaphore(size, size);
}

public void enqueue(T item) throws InterruptedException {


semProducer.acquire();
synchronized (this) {
buffer.add(item);
}
semConsumer.release();
}

public T dequeue() throws InterruptedException {


semConsumer.acquire();
T item;
synchronized (this) {
item = buffer.poll();
}
semProducer.release();
return item;
}
}

Producer and Consumer Threads:

We’ll create producer and consumer threads that use the enqueue() and
dequeue() methods of the BoundedBuffer .

public class Producer implements Runnable {


private BoundedBuffer<Integer> buffer;

public Producer(BoundedBuffer<Integer> buffer) {


this.buffer = buffer;
}

@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
buffer.enqueue(i);
System.out.println("Produced: " + i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

public class Consumer implements Runnable {


private BoundedBuffer<Integer> buffer;

public Consumer(BoundedBuffer<Integer> buffer) {


this.buffer = buffer;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
int item = buffer.dequeue();
System.out.println("Consumed: " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

Main Method to Run the Example:

We’ll set up the buffer and start the producer and consumer threads.

public class ProducerConsumerExample {


public static void main(String[] args) {
BoundedBuffer<Integer> buffer = new BoundedBuffer<>(5);
Thread producerThread = new Thread(new Producer(buffer));
Thread consumerThread = new Thread(new Consumer(buffer));

producerThread.start();
consumerThread.start();

try {
producerThread.join();
consumerThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

CountingSemaphore Class: Manages permits to control access to the


buffer. The acquire() method blocks when no permits are available, and
the release() method releases a permit, potentially unblocking waiting
threads.

BoundedBuffer Class: Uses two semaphores to manage producer and


consumer access. The enqueue() method allows producers to add items
when space is available, and the dequeue() method allows consumers to
remove items when they exist.

Producer and Consumer Threads: Continuously produce and consume


items, respectively, demonstrating the synchronization achieved through
semaphores.

This implementation demonstrates how semaphores can effectively manage


synchronization between producer and consumer threads, ensuring that the
buffer does not overflow or underflow.
Recall that we can use a binary semaphore to exercise mutual exclusion,
however, any thread is free to signal the semaphore, not just the one that
acquired it.

Rate Limiting Using Token Bucket Filter

This is an actual interview question asked at Uber and Oracle. Imagine you
have a bucket that gets filled with tokens at the rate of 1 token per second.
The bucket can hold a maximum of N tokens. Implement a thread-safe class
that lets threads get a token when one is available. If no token is available,
then the token-requesting threads should block. The class should expose an
API called getToken that various threads can call to get a token.

One application of these algorithms is shaping network traffic flows. This


particular problem is interesting because the majority of candidates
incorrectly start with a multithreaded approach when taking a stab at the
problem. One is tempted to create a background thread to fill the bucket
with tokens at regular intervals but there is a far simpler solution devoid of
threads and a message to make judicious use of threads. This question tests a
candidate’s comprehension prowess as well as concurrency knowledge.

The key to the problem is to find a way to track the number of available
tokens when a consumer requests for a token. Note the rate at which the
tokens are being generated is constant. So if we know when the token bucket
was instantiated and when a consumer called getToken() we can take the
difference of the two instants and know the number of possible tokens we
would have collected so far. However, we’ll need to tweak our solution to
account for the max number of tokens the bucket can hold.

Implementing a thread-safe Token Bucket class in Java requires careful


management of token generation and consumption to ensure that multiple
threads can safely request tokens without conflicts. The token bucket
algorithm is commonly used for rate limiting, where tokens are added to a
bucket at a fixed rate, and threads consume tokens when performing
actions. If no tokens are available, requesting threads should block until
tokens become available.

Key Concepts:

Token Generation Rate: Tokens are added to the bucket at a constant


rate, typically one token per second.

Maximum Capacity: The bucket has a maximum capacity (N tokens) to


prevent it from holding more tokens than specified.

Thread Safety: Multiple threads may request tokens simultaneously, so


the implementation must handle concurrent access appropriately.

Implementation Strategy:

Instead of using a separate thread to add tokens at regular intervals, we can


calculate the number of tokens available based on the elapsed time since the
last token request. This approach avoids the complexity of managing
additional threads and ensures that token generation is handled efficiently.

Implementation Steps:

1. Define the TokenBucket Class:

2. We’ll create a TokenBucket class with methods to initialize the bucket,


request tokens, and manage token availability.
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TokenBucket {


private final int maxTokens;
private double availableTokens;
private long lastRefillTimestamp;
private final Lock lock = new ReentrantLock();
private final Condition tokensAvailable = lock.newCondition();

public TokenBucket(int maxTokens) {


this.maxTokens = maxTokens;
this.availableTokens = maxTokens;
this.lastRefillTimestamp = System.nanoTime();
}

private void refill() {


long now = System.nanoTime();
double tokensToAdd = (now - lastRefillTimestamp) / 1_000_000_000.0;
availableTokens = Math.min(maxTokens, availableTokens + tokensToAdd);
lastRefillTimestamp = now;
}

public void getToken() throws InterruptedException {


lock.lock();
try {
while (availableTokens < 1) {
refill();
if (availableTokens < 1) {
long waitTime = (long) ((1 - availableTokens) * 1_000_000_00
tokensAvailable.awaitNanos(waitTime);
}
}
availableTokens--;
tokensAvailable.signalAll();
} finally {
lock.unlock();
}
}
}

Fields:

maxTokens : Maximum capacity of the bucket.

availableTokens : Current number of tokens available.

lastRefillTimestamp : Timestamp of the last refill operation.

lock : A ReentrantLock to ensure thread safety.

tokensAvailable : A Condition to manage threads waiting for tokens.

Constructor:

Initializes the bucket with the maximum number of tokens and sets the
last refill timestamp to the current time.
refill Method:

Calculates the number of tokens to add based on the elapsed time since
the last refill.

Ensures that the number of available tokens does not exceed the
maximum capacity.

Updates the last refill timestamp to the current time.

getToken Method:

Acquires the lock to ensure thread safety.

Refills the bucket with tokens based on the elapsed time.

If no tokens are available, calculates the time to wait for the next token to
become available and waits on the tokensAvailable condition.

Once a token is available, decrements the available tokens and signals all
waiting threads.

Releases the lock.

Testing the TokenBucket Class:

We’ll create a test class to simulate multiple threads requesting tokens


from the bucket.

public class TokenBucketTest {


public static void main(String[] args) {
final TokenBucket tokenBucket = new TokenBucket(5);

Runnable task = () -> {


try {
tokenBucket.getToken();
System.out.println(Thread.currentThread().getName() + " acquired
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};

for (int i = 0; i < 10; i++) {


new Thread(task).start();
try {
Thread.sleep(500); // Start a new thread every 500 milliseconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
Creates a TokenBucket instance with a maximum capacity of 5 tokens.

Defines a task where each thread attempts to acquire a token and prints a
message upon success.

Starts 10 threads, each attempting to acquire a token, with a 500-


millisecond delay between thread starts.

This implementation provides a thread-safe TokenBucket class that allows


multiple threads to request tokens at a controlled rate. By calculating token
availability based on elapsed time and using locking mechanisms to manage
concurrent access, the solution ensures that threads block when no tokens
are available and proceed when tokens become available.

For a visual explanation of implementing a rate limiter using the token


bucket algorithm in Java, you may find the following video helpful:

Simple Rate Limiter in Java with Token Bucket Algorithm


Share

Watch on

Simple Rate Limiter in Java with Token Bucket Algorithm

We need to think about the following three cases to roll out our algorithm.
Let’s assume the maximum allowed tokens our bucket can hold is 5. The last
request for token was more than 5 seconds ago: In this scenario, each
elapsed second would have generated one token which may total more than
five tokens since the last request was more than 5 seconds ago. We simply
need to set the maximum tokens available to 5 since that is the most the
bucket will hold and return one token out of those 5. The last request for
token was within a window of 5 seconds: In this scenario, we need to
calculate the new tokens generated since the last request and add them to
the unused tokens we already have. We then return 1 token from the count.
The last request was within a 5-second window and all the tokens are used
up: In this scenario, there’s no option but to sleep for a whole second to
guarantee that a token would become available and then let the thread
return. While we sleep(), the monitor would still be held by the token-
requesting thread and any new threads invoking getToken would get
blocked, waiting for the monitor to become available.

You can see the final solution comes out to be very trivial without the
requirement for creating a bucket-filling thread of sorts, that runs
perpetually and increments a counter every second to reflect the addition of
a token to the bucket. Many candidates initially get off-track by taking this
approach. Though you might be able to solve this problem using the
mentioned approach, the code would unnecessarily be complex and
unwieldy. Note we achieve thread-safety by simply adding synchronized to
the getToken method. We can have finer grained synchronization inside the
method, but that wouldn’t help since the entire code snippet within the
method is critical and would be guarded by a lock.

To enhance the previous implementation of the TokenBucket class, we'll


introduce two key features:

1. Granting Tokens to Threads in FIFO Order: Ensuring that threads receive


tokens in the order they requested them.

2. Generalizing the Solution for Any Rate of Token Generation: Allowing


tokens to be generated at a configurable rate, not limited to one token per
second.

Implementation Details:

1. FIFO Order for Token Granting:

2. To maintain the order of token requests, we’ll use a BlockingQueue to hold


threads waiting for tokens. Each thread will place a marker (e.g., its own
reference) into the queue when requesting a token and will wait until it's
at the front of the queue before attempting to acquire a token. This
approach ensures that threads are served in the order they arrived.

3. Configurable Token Generation Rate:

4. We’ll introduce a tokenGenerationRate parameter to specify the number


of tokens added to the bucket per second. This allows the token
generation rate to be adjusted as needed.
Updated TokenBucket Class:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TokenBucket {


private final int maxTokens;
private double availableTokens;
private long lastRefillTimestamp;
private final double tokenGenerationRate; // Tokens per second
private final Lock lock = new ReentrantLock();
private final BlockingQueue<Thread> waitingThreads = new LinkedBlockingQueue

public TokenBucket(int maxTokens, double tokenGenerationRate) {


this.maxTokens = maxTokens;
this.tokenGenerationRate = tokenGenerationRate;
this.availableTokens = maxTokens;
this.lastRefillTimestamp = System.nanoTime();
}

private void refill() {


long now = System.nanoTime();
double tokensToAdd = ((now - lastRefillTimestamp) / 1_000_000_000.0) * t
availableTokens = Math.min(maxTokens, availableTokens + tokensToAdd);
lastRefillTimestamp = now;
}

public void getToken() throws InterruptedException {


Thread currentThread = Thread.currentThread();
waitingThreads.put(currentThread); // Add the current thread to the queu

while (true) {
lock.lock();
try {
if (waitingThreads.peek() == currentThread) { // Check if it's t
refill();
if (availableTokens >= 1) {
availableTokens--;
waitingThreads.take(); // Remove the thread from the que
return;
}
}
} finally {
lock.unlock();
}
Thread.sleep(100); // Sleep briefly before retrying
}
}
}

Fields:

maxTokens : Maximum capacity of the bucket.

availableTokens : Current number of tokens available.

lastRefillTimestamp : Timestamp of the last refill operation.


tokenGenerationRate : Number of tokens generated per second.

lock : A ReentrantLock to ensure thread safety.

waitingThreads : A BlockingQueue to manage threads in FIFO order.

Constructor:

Initializes the bucket with the specified maximum tokens and token
generation rate.

Sets the initial available tokens to the maximum and records the current
time.

refill Method:

Calculates the number of tokens to add based on the elapsed time and the
token generation rate.

Ensures that the number of available tokens does not exceed the
maximum capacity.

Updates the last refill timestamp to the current time.

getToken Method:

Adds the current thread to the waitingThreads queue.

Enters a loop where it acquires the lock and checks if it’s the thread’s turn
(i.e., it’s at the front of the queue).

Refills the bucket with tokens based on the elapsed time.

If sufficient tokens are available, decrements the available tokens,


removes itself from the queue, and exits.

If not enough tokens are available or it’s not the thread’s turn, releases the
lock and sleeps briefly before retrying.

Testing the Updated TokenBucket Class:

public class TokenBucketTest {


public static void main(String[] args) {
final TokenBucket tokenBucket = new TokenBucket(5, 2.0); // 5 tokens max

Runnable task = () -> {


try {
tokenBucket.getToken();
System.out.println(Thread.currentThread().getName() + " acquired
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};

for (int i = 0; i < 10; i++) {


new Thread(task).start();
try {
Thread.sleep(500); // Start a new thread every 500 milliseconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}

Creates a TokenBucket instance with a maximum capacity of 5 tokens and


a generation rate of 2 tokens per second.

Defines a task where each thread attempts to acquire a token and prints a
message upon success.

Starts 10 threads, each attempting to acquire a token, with a 500-


millisecond delay between thread starts.

This enhanced implementation of the TokenBucket class ensures that threads


receive tokens in the order they requested them (FIFO) and allows for a
configurable token generation rate. By managing a queue of waiting threads
and calculating token availability based on elapsed time and the specified
rate, the solution provides flexible and fair token distribution among
multiple threads.

To implement a thread-safe Token Bucket Filter using a dedicated thread to


add tokens at regular intervals, we can follow these steps:

Define the TokenBucketFilter Class:

Fields:

maxTokens : Maximum capacity of the bucket.

currentTokens : Current number of tokens available.

lock : An object to synchronize access to shared resources.

daemonThread : The thread responsible for adding tokens at regular


intervals.

running : A flag to control the daemon thread's execution.

Constructor:
Initializes the bucket with the specified maximum tokens.

Starts the daemon thread to add tokens.

Methods:

getToken() : Allows threads to acquire a token if available; blocks if no


tokens are available.

daemonThread() : Runs in a separate thread, adding tokens to the bucket at


one-second intervals.

stop() : Stops the daemon thread gracefully.

1. Implement the TokenBucketFilter Class:

public class TokenBucketFilter {


private final int maxTokens;
private int currentTokens;
private final Object lock = new Object();
private Thread daemonThread;
private volatile boolean running = true;

public TokenBucketFilter(int maxTokens) {


this.maxTokens = maxTokens;
this.currentTokens = 0;
startDaemonThread();
}

private void startDaemonThread() {


daemonThread = new Thread(this::daemonThread);
daemonThread.setDaemon(true);
daemonThread.start();
}

private void daemonThread() {


while (running) {
synchronized (lock) {
if (currentTokens < maxTokens) {
currentTokens++;
lock.notifyAll(); // Notify waiting threads that a token is
}
}
try {
Thread.sleep(1000); // Add a token every second
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

public void getToken() throws InterruptedException {


synchronized (lock) {
while (currentTokens == 0) {
lock.wait(); // Wait until a token is available
}
currentTokens--;
}
}

public void stop() {


running = false;
daemonThread.interrupt();
}
}

Test the TokenBucketFilter Class:

public class TokenBucketFilterTest {


public static void main(String[] args) {
TokenBucketFilter tokenBucketFilter = new TokenBucketFilter(5);

Runnable task = () -> {


try {
tokenBucketFilter.getToken();
System.out.println(Thread.currentThread().getName() + " acquired
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};

for (int i = 0; i < 10; i++) {


new Thread(task).start();
try {
Thread.sleep(500); // Start a new thread every 500 milliseconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

// Allow some time for threads to finish


try {
Thread.sleep(10000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

// Stop the daemon thread


tokenBucketFilter.stop();
}
}

Token Addition:

The daemonThread() method runs in a separate thread, adding one token


to the bucket every second until the bucket reaches its maximum
capacity.

The running flag controls the execution of the daemon thread, allowing
for a graceful shutdown.

Token Acquisition:
The getToken() method allows threads to acquire a token. If no tokens
are available ( currentTokens == 0 ), the thread waits ( lock.wait() ) until
notified ( lock.notifyAll() ) that a token has been added.

Synchronization:

The lock object ensures that access to currentTokens is thread-safe,


preventing race conditions.

Daemon Thread:

The token-adding thread is set as a daemon thread


( daemonThread.setDaemon(true) ), meaning it won't prevent the JVM from
exiting if all other threads have finished.

Graceful Shutdown:

The stop() method sets the running flag to false and interrupts the
daemon thread, allowing it to terminate gracefully.

Considerations:

Thread Safety:

The use of synchronization ensures that multiple threads can safely


interact with the token bucket without causing inconsistent states.

Performance:

The lock.notifyAll() call wakes up all waiting threads whenever a token


is added. Depending on the number of waiting threads, this could lead to
a thundering herd problem. In such cases, more sophisticated
concurrency control mechanisms may be considered.

Daemon Threads:

Daemon threads are suitable for background tasks that should not
prevent the JVM from exiting. However, care should be taken to manage
their lifecycle appropriately to avoid resource leaks.

This implementation provides a straightforward approach to managing a


token bucket filter using threads, ensuring that token requests are handled
in a thread-safe manner with tokens being added at regular intervals by a
dedicated daemon thread.

Never start a thread in a constructor as the child thread can attempt to use the
not-yet-fully constructed object using this. This is an anti-pattern. Some
candidates present this solution when attempting to solve token bucket filter
problem using threads. However, when checked, few candidates can reason
why starting threads in a constructor is a bad choice.

There are two ways to overcome this problem, the naive but correct solution
is to start the daemon thread outside of the MultithreadedTokenBucketFilter
object. However, the con of this approach is that the management of the
daemon thread spills outside the class. Ideally, we want the class to
encapsulate all the operations related with the management of the token
bucket filter and only expose the public API to the consumers of our class, as
per good object orientated design.

To implement a token bucket filter using a factory design pattern in Java,


we’ll follow these steps:

1. Define an abstract TokenBucketFilter class: This serves as the base class


for our token bucket filter implementation.

2. Create a private nested class MultithreadedTokenBucketFilter : This class


extends TokenBucketFilter and contains the actual implementation.

3. Implement a factory class TokenBucketFilterFactory : This class provides


a method makeTokenBucketFilter() to create and initialize instances of
MultithreadedTokenBucketFilter .

4. Ensure the daemon thread starts only after full construction: The
factory method will start the daemon thread before returning the fully
constructed object.

5. Prevent direct instantiation of MultithreadedTokenBucketFilter : By


nesting it within the factory class and making it private, we ensure that
consumers can only create instances through the factory.

Here’s how you can implement this:

// Abstract class representing the Token Bucket Filter


public abstract class TokenBucketFilter {
public abstract void getToken() throws InterruptedException;
}

// Factory class to create instances of TokenBucketFilter


public class TokenBucketFilterFactory {
// Private nested class implementing the TokenBucketFilter
private static class MultithreadedTokenBucketFilter extends TokenBucketFilte
private final int maxTokens;
private int currentTokens;
private final long refillInterval;
private final Thread daemonThread;

private MultithreadedTokenBucketFilter(int maxTokens, long refillInterva


this.maxTokens = maxTokens;
this.currentTokens = maxTokens;
this.refillInterval = refillInterval;

// Initialize and configure the daemon thread


this.daemonThread = new Thread(() -> {
try {
while (true) {
refill();
Thread.sleep(refillInterval);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
this.daemonThread.setDaemon(true);
}

// Method to refill tokens


private synchronized void refill() {
if (currentTokens < maxTokens) {
currentTokens++;
System.out.println("Token added. Current tokens: " + currentToke
}
}

// Method to acquire a token


@Override
public synchronized void getToken() throws InterruptedException {
while (currentTokens == 0) {
System.out.println("No tokens available; waiting...");
wait();
}
currentTokens--;
System.out.println("Token acquired. Tokens left: " + currentTokens);
notifyAll();
}

// Start the daemon thread


private void startDaemon() {
this.daemonThread.start();
}
}

// Factory method to create and initialize the token bucket filter


public static TokenBucketFilter makeTokenBucketFilter(int maxTokens, long re
MultithreadedTokenBucketFilter filter = new MultithreadedTokenBucketFilt
filter.startDaemon();
return filter;
}
}

Usage Example:
public class Main {
public static void main(String[] args) throws InterruptedException {
// Create a token bucket filter with a capacity of 5 tokens and a refill
TokenBucketFilter filter = TokenBucketFilterFactory.makeTokenBucketFilte

// Simulate multiple threads acquiring tokens


for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
filter.getToken();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
Thread.sleep(200); // Slight delay between thread starts
}
}
}

Abstract Class TokenBucketFilter : Defines the contract for acquiring


tokens.

Private Nested Class MultithreadedTokenBucketFilter : Implements the


token bucket algorithm with a daemon thread that refills tokens at a
specified interval.

Factory Class TokenBucketFilterFactory : Provides the


makeTokenBucketFilter() method to create instances of
MultithreadedTokenBucketFilter . The daemon thread is started before the
object is returned, ensuring it's fully constructed.

Thread Safety: The getToken() and refill() methods are synchronized


to handle concurrent access.

Daemon Thread: The refill thread runs as a daemon, meaning it won’t


prevent the JVM from exiting if all other threads have finished.

This design ensures that consumers can only create token bucket filter
instances through the factory, enforcing proper initialization and
encapsulation.

Thread Safe Deferred Callback


Asynchronous programming involves being able to execute functions at a future
occurrence of some event. Designing a thread-safe deferred callback class
becomes a challenging interview question.

To implement a thread-safe deferred callback mechanism in Java, we can


create a class that allows the registration of callback methods to be executed
after a specified time interval. We’ll utilize Java’s ScheduledExecutorService to
handle the scheduling and execution of these callbacks. This approach
ensures thread safety and efficient management of delayed tasks.

Implementation Steps:

Define the DeferredCallbackExecutor Class:

This class will manage the registration and execution of deferred


callbacks.

It will use a ScheduledExecutorService to schedule tasks for future


execution.

A thread-safe data structure, such as ConcurrentLinkedQueue , will be used


to store pending callbacks.
Provide a Method to Register Callbacks:

The method will accept a Runnable representing the callback and a delay
interval in seconds.

It will schedule the callback for execution after the specified delay.

Ensure Thread Safety:

By using ScheduledExecutorService and thread-safe collections, we ensure


that multiple threads can register callbacks concurrently without issues.

Implement Proper Resource Management:

Provide methods to start and stop the executor service to manage


resources appropriately.

Here’s how you can implement this:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;

public class DeferredCallbackExecutor {

// Scheduled executor service for scheduling tasks


private final ScheduledExecutorService scheduler;
// Queue to hold pending callbacks
private final ConcurrentLinkedQueue<Runnable> pendingCallbacks;
// Flag to indicate if the executor is running
private final AtomicBoolean isRunning;

// Constructor to initialize the executor with a specified number of threads


public DeferredCallbackExecutor(int threadPoolSize) {
this.scheduler = Executors.newScheduledThreadPool(threadPoolSize);
this.pendingCallbacks = new ConcurrentLinkedQueue<>();
this.isRunning = new AtomicBoolean(true);
}

// Method to register a callback with a delay in seconds


public void registerCallback(Runnable callback, long delayInSeconds) {
if (isRunning.get()) {
// Schedule the callback for execution after the specified delay
ScheduledFuture<?> scheduledFuture = scheduler.schedule(() -> {
try {
// Execute the callback
callback.run();
} finally {
// Remove the callback from the pending queue after executio
pendingCallbacks.remove(callback);
}
}, delayInSeconds, TimeUnit.SECONDS);
// Add the callback to the pending queue
pendingCallbacks.add(() -> {
try {
scheduledFuture.get();
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Callback execution interrupted",
}
});
} else {
throw new IllegalStateException("Executor has been shut down");
}
}

// Method to shut down the executor gracefully


public void shutdown() {
isRunning.set(false);
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}

// Method to forcefully shut down the executor


public void shutdownNow() {
isRunning.set(false);
scheduler.shutdownNow();
}
}

Usage Example:

public class Main {


public static void main(String[] args) {
// Create an instance of DeferredCallbackExecutor with a thread pool siz
DeferredCallbackExecutor executor = new DeferredCallbackExecutor(2);

// Register a callback to be executed after 5 seconds


executor.registerCallback(() -> {
System.out.println("Callback executed after 5 seconds");
}, 5);

// Register another callback to be executed after 10 seconds


executor.registerCallback(() -> {
System.out.println("Callback executed after 10 seconds");
}, 10);

// Wait for some time before shutting down the executor


try {
Thread.sleep(15000); // Wait for 15 seconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

// Shut down the executor gracefully


executor.shutdown();
}
}

DeferredCallbackExecutor Class:
Manages the scheduling and execution of deferred callbacks.

Uses a ScheduledExecutorService to handle delayed task execution.

Maintains a thread-safe queue ( ConcurrentLinkedQueue ) to keep track of


pending callbacks.

Employs an AtomicBoolean to indicate whether the executor is running,


ensuring thread-safe state management.

registerCallback Method:

Accepts a Runnable callback and a delay interval in seconds.

Schedules the callback for execution after the specified delay using the
scheduler .

Adds the callback to the pendingCallbacks queue for tracking.

Ensures that the callback is removed from the queue after execution to
prevent memory leaks.

shutdown and shutdownNow Methods:

Provide mechanisms to gracefully or forcefully shut down the executor


service.

Ensure that no new callbacks can be registered once the executor is shut
down.

Await termination of existing tasks and handle interruptions


appropriately.

Usage Example:

Demonstrates how to create an instance of DeferredCallbackExecutor .

Shows how to register callbacks with different delay intervals.

Illustrates the importance of shutting down the executor after use to


release resources.

Thread Safety Considerations:

The use of ScheduledExecutorService ensures that tasks are executed in a


thread-safe manner.

ConcurrentLinkedQueue allows for safe concurrent access to the queue of


pending callbacks.
AtomicBoolean provides a thread-safe flag to manage the running state of
the executor.

Proper synchronization is maintained during callback registration and


execution to prevent race conditions.

To design a thread-safe deferred callback mechanism in Java without relying


on a busy-waiting thread, we can utilize a ScheduledExecutorService in
conjunction with a PriorityBlockingQueue . This approach allows us to
efficiently manage and execute callbacks at specified future times without
unnecessary CPU consumption.

Design Overview:

1. Callback Interface: Define a Callback interface representing the tasks to


be executed.

2. Callback Implementation: Implement the Callback interface in concrete


classes that define the specific tasks.

3. DeferredCallbackExecutor Class: Create a class responsible for


managing the scheduling and execution of callbacks. This class will:

Maintain a PriorityBlockingQueue to store callbacks ordered by their


scheduled execution time.

Use a single execution thread to monitor the queue and execute callbacks
when their scheduled time arrives.

Ensure thread safety by synchronizing access to shared resources and


managing the execution thread’s sleep duration dynamically.

Implementation:

1. Define the Callback Interface:

public interface Callback {


void execute();
}

2. Implement the Callback Interface:

public class MyCallback implements Callback {


private final String message;
public MyCallback(String message) {
this.message = message;
}

@Override
public void execute() {
System.out.println(message);
}
}

3. Implement the DeferredCallbackExecutor Class:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;

public class DeferredCallbackExecutor {


private final ScheduledExecutorService scheduler;
private final PriorityBlockingQueue<ScheduledCallback> callbackQueue;
private final AtomicBoolean isRunning;

public DeferredCallbackExecutor(int threadPoolSize) {


this.scheduler = Executors.newScheduledThreadPool(threadPoolSize);
this.callbackQueue = new PriorityBlockingQueue<>();
this.isRunning = new AtomicBoolean(true);
startExecutionThread();
}

private void startExecutionThread() {


Thread executionThread = new Thread(() -> {
while (isRunning.get()) {
try {
ScheduledCallback scheduledCallback = callbackQueue.take();
long currentTime = System.currentTimeMillis();
long delay = scheduledCallback.getScheduledTime() - currentT
if (delay > 0) {
Thread.sleep(delay);
}
scheduledCallback.getCallback().execute();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
executionThread.setDaemon(true);
executionThread.start();
}

public void registerCallback(Callback callback, long delayInSeconds) {


if (isRunning.get()) {
long scheduledTime = System.currentTimeMillis() + TimeUnit.SECONDS.t
ScheduledCallback scheduledCallback = new ScheduledCallback(callback
callbackQueue.put(scheduledCallback);
} else {
throw new IllegalStateException("Executor has been shut down");
}
}

public void shutdown() {


isRunning.set(false);
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}

public void shutdownNow() {


isRunning.set(false);
scheduler.shutdownNow();
}

private static class ScheduledCallback implements Comparable<ScheduledCallba


private final Callback callback;
private final long scheduledTime;

public ScheduledCallback(Callback callback, long scheduledTime) {


this.callback = callback;
this.scheduledTime = scheduledTime;
}

public Callback getCallback() {


return callback;
}

public long getScheduledTime() {


return scheduledTime;
}

@Override
public int compareTo(ScheduledCallback other) {
return Long.compare(this.scheduledTime, other.scheduledTime);
}
}
}

Usage Example:

public class Main {


public static void main(String[] args) {
DeferredCallbackExecutor executor = new DeferredCallbackExecutor(1);

executor.registerCallback(new MyCallback("Callback executed after 5 seco


executor.registerCallback(new MyCallback("Callback executed after 10 sec

try {
Thread.sleep(15000); // Wait for 15 seconds to allow callbacks to ex
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

executor.shutdown();
}
}

Callback Interface and Implementation: The Callback interface defines


the contract for tasks to be executed, and MyCallback provides a concrete
implementation that prints a message.
DeferredCallbackExecutor Class:

Manages a PriorityBlockingQueue to store ScheduledCallback objects,


which are ordered by their scheduled execution time.

The startExecutionThread method starts a daemon thread that


continuously monitors the queue and executes callbacks when their
scheduled time arrives.

The registerCallback method allows clients to register callbacks with a


specified delay in seconds.

The shutdown and shutdownNow methods provide mechanisms to


gracefully or forcefully shut down the executor service.

Thread Safety: The use of PriorityBlockingQueue ensures that the queue


operations are thread-safe. The AtomicBoolean isRunning manages the
running state of the executor, ensuring that no new callbacks can be
registered once the executor is shut down.

Considerations:

The DeferredCallbackExecutor class uses a single execution thread to


monitor and execute callbacks. This design avoids the need for a busy-
waiting thread, efficiently managing CPU resources.

The PriorityBlockingQueue ensures that callbacks are executed in the


correct order based on their scheduled times.

Proper synchronization is maintained during callback registration and


execution to prevent race conditions.

This implementation provides a robust and thread-safe mechanism for


deferring the execution of callbacks in Java, efficiently managing resources
without the need for busy-waiting threads.

To implement a thread-safe deferred callback mechanism in Java, we can


utilize a PriorityQueue to manage callbacks ordered by their scheduled
execution times, a ReentrantLock to ensure mutual exclusion, and a
Condition variable to coordinate between threads.

import java.util.PriorityQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class DeferredCallbackExecutor {


private final PriorityQueue<ScheduledCallback> callbackQueue;
private final ReentrantLock lock;
private final Condition condition;
private final Thread executionThread;
private volatile boolean isRunning;

public DeferredCallbackExecutor() {
this.callbackQueue = new PriorityQueue<>();
this.lock = new ReentrantLock();
this.condition = lock.newCondition();
this.isRunning = true;

this.executionThread = new Thread(() -> {


while (isRunning) {
try {
lock.lock();
if (callbackQueue.isEmpty()) {
condition.await();
} else {
ScheduledCallback scheduledCallback = callbackQueue.peek
long currentTime = System.currentTimeMillis();
long delay = scheduledCallback.getScheduledTime() - curr
if (delay > 0) {
condition.awaitNanos(delay * 1_000_000);
} else {
callbackQueue.poll();
scheduledCallback.getCallback().execute();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
});
executionThread.setDaemon(true);
executionThread.start();
}

public void registerCallback(Callback callback, long delayInSeconds) {


if (!isRunning) {
throw new IllegalStateException("Executor has been shut down");
}
long scheduledTime = System.currentTimeMillis() + delayInSeconds * 1000;
ScheduledCallback scheduledCallback = new ScheduledCallback(callback, sc
lock.lock();
try {
callbackQueue.add(scheduledCallback);
condition.signal();
} finally {
lock.unlock();
}
}

public void shutdown() {


isRunning = false;
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}

private static class ScheduledCallback implements Comparable<ScheduledCallba


private final Callback callback;
private final long scheduledTime;

public ScheduledCallback(Callback callback, long scheduledTime) {


this.callback = callback;
this.scheduledTime = scheduledTime;
}
public Callback getCallback() {
return callback;
}

public long getScheduledTime() {


return scheduledTime;
}

@Override
public int compareTo(ScheduledCallback other) {
return Long.compare(this.scheduledTime, other.scheduledTime);
}
}
}

Callback Interface and Implementation: The Callback interface defines the


contract for tasks to be executed, and MyCallback provides a concrete
implementation that prints a message.

DeferredCallbackExecutor Class:

Manages a PriorityQueue to store ScheduledCallback objects, which are


ordered by their scheduled execution time.

The executionThread continuously monitors the queue and executes


callbacks when their scheduled time arrives.

The registerCallback method allows clients to register callbacks with a


specified delay in seconds.

The shutdown method stops the executor and signals the execution thread
to terminate.

Thread Safety: The use of ReentrantLock and Condition ensures that


access to the callback queue is thread-safe, and the execution thread can
be properly signaled when new callbacks are registered.

Considerations:

The DeferredCallbackExecutor class uses a single execution thread to


monitor and execute callbacks, avoiding the need for a busy-waiting
thread and efficiently managing CPU resources.

The PriorityQueue ensures that callbacks are executed in the correct


order based on their scheduled times.

Proper synchronization is maintained during callback registration and


execution to prevent race conditions.
This implementation provides a robust and thread-safe mechanism for
deferring the execution of callbacks in Java, efficiently managing resources
without the need for busy-waiting threads.

For a visual explanation of PriorityQueues in Java with custom comparators,


you might find the following video helpful:

Priority Queue Explained | Min and Max Heap | Custom Comparator


Share

Watch on

Priority Queue Explained | Min and Max Heap | Custom Comparator

To address the scenario where a callback is registered to execute after 8


seconds, and another callback is registered 3 seconds later to execute after 1
second, we need to ensure that the callback with the shorter total delay (i.e.,
the one registered later) executes first.

Key Considerations:

Callback Timing: The total delay for a callback is the sum of the time
elapsed since its registration and the delay specified at registration.

Priority Queue Ordering: The PriorityQueue in Java orders elements


based on their natural ordering or a provided comparator. In our case,
we need to order callbacks by their scheduled execution time.

Usage Example:

public class Main {


public static void main(String[] args) {
DeferredCallbackExecutor executor = new DeferredCallbackExecutor();

// Register a callback to execute after 8 seconds


executor.registerCallback(new MyCallback("Callback executed after 8 seco

// Wait for 3 seconds before registering the next callback


try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

// Register another callback to execute after 1 second


executor.registerCallback(new MyCallback("Callback executed after 1 seco

// Wait for 10 seconds to allow callbacks to execute


try {
Thread.sleep(10000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

executor.shutdown();
}
}

Expected Output:

Callback executed after 1 second


Callback executed after 8 seconds

Callback Registration: The first callback is registered to execute after 8


seconds. Three seconds later, another callback is registered to execute
after 1 second. The total delay for the second callback is 4 seconds (3
seconds wait + 1 second delay).

Execution Order: The DeferredCallbackExecutor uses a PriorityQueue to


manage callbacks based on their scheduled execution times. The second
callback, with a total delay of 4 seconds, is scheduled to execute before
the first callback, which has a total delay of 11 seconds.

Thread Coordination: The executor thread waits for the earliest callback
to become due and executes it. It uses a Condition variable to be notified
when a new callback is registered.

Considerations:

Thread Safety: The use of ReentrantLock and Condition ensures that


access to the callback queue is thread-safe, and the execution thread can
be properly signaled when new callbacks are registered.

Callback Execution: Callbacks are executed in the order of their


scheduled times, ensuring that the callback with the shorter total delay
executes first.
Implementing Semaphore

Java does provide its own implementation of Semaphore, however, Java’s


semaphore is initialized with an initial number of permits, rather than the
maximum possible permits and the developer is expected to take care of
always releasing the intended number of maximum permits. Briefly, a
semaphore is a construct that allows some threads to access a fixed set of
resources in parallel. Always think of a semaphore as having a fixed number
of permits to give out. Once all the permits are given out, requesting threads,
need to wait for a permit to be returned before proceeding forward. Your
task is to implement a semaphore which takes in its constructor the
maximum number of permits allowed and is also initialized with the same
number of permits.

To implement a custom semaphore in Java that simulates acquiring and


releasing permits, we can utilize the wait() and notify() methods for
thread synchronization. This approach allows us to control access to a
shared resource by managing the number of available permits.
Custom Semaphore Implementation:

public class CustomSemaphore {


private final int maxPermits;
private int usedPermits;

public CustomSemaphore(int permits) {


if (permits <= 0) {
throw new IllegalArgumentException("Number of permits must be positi
}
this.maxPermits = permits;
this.usedPermits = 0;
}

// Acquire a permit
public synchronized void acquire() throws InterruptedException {
while (usedPermits == maxPermits) {
wait(); // Wait until a permit is available
}
usedPermits++;
notify(); // Notify any waiting threads
}

// Release a permit
public synchronized void release() {
while (usedPermits == 0) {
try {
wait(); // Wait until a permit is acquired
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
usedPermits--;
notify(); // Notify any waiting threads
}
}

Constructor ( CustomSemaphore(int permits) ): Initializes the semaphore


with a specified number of permits. If the number of permits is less than
or equal to zero, an IllegalArgumentException is thrown.

acquire() Method: This method is called by a thread to acquire a permit.


If all permits are in use ( usedPermits == maxPermits ), the thread waits
until a permit becomes available. Once a permit is acquired, usedPermits

is incremented, and any waiting threads are notified.

release() Method: This method is called by a thread to release a permit.


If no permits are in use ( usedPermits == 0 ), the thread waits until a
permit is acquired. Once a permit is released, usedPermits is
decremented, and any waiting threads are notified.

Usage Example:
public class SemaphoreTest {
public static void main(String[] args) {
CustomSemaphore semaphore = new CustomSemaphore(1); // Binary semaphore

// Thread 1: Acquire and release the semaphore


Thread t1 = new Thread(() -> {
try {
semaphore.acquire();
System.out.println("Thread 1 acquired the semaphore.");
Thread.sleep(1000); // Simulate some work
semaphore.release();
System.out.println("Thread 1 released the semaphore.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});

// Thread 2: Acquire and release the semaphore


Thread t2 = new Thread(() -> {
try {
semaphore.acquire();
System.out.println("Thread 2 acquired the semaphore.");
Thread.sleep(1000); // Simulate some work
semaphore.release();
System.out.println("Thread 2 released the semaphore.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});

t1.start();
t2.start();
}
}

Expected Output:

Thread 1 acquired the semaphore.


Thread 1 released the semaphore.
Thread 2 acquired the semaphore.
Thread 2 released the semaphore.

Thread 1 acquires the semaphore, performs some work (simulated by


Thread.sleep(1000) ), and then releases the semaphore.

Thread 2 waits for Thread 1 to release the semaphore, acquires it,


performs its work, and then releases it.

This implementation ensures that only one thread can access the shared
resource at a time, effectively simulating a binary semaphore.

Considerations:
Thread Safety: The synchronized keyword ensures that only one thread
can execute either the acquire() or release() method at a time,
preventing race conditions.

Waiting and Notification: The wait() and notify() methods are used to
manage the availability of permits. Threads wait when no permits are
available and are notified when a permit is released.

Interrupt Handling: In the release() method, if a thread is waiting, it


may be interrupted. The InterruptedException is caught, and the thread's
interrupt status is restored by calling Thread.currentThread().interrupt() .

This custom semaphore implementation provides a basic mechanism for


controlling access to a shared resource in a multithreaded environment.

For a deeper understanding of semaphores and their usage in Java, you


might find the following helpful: Semaphore in Java — GeeksforGeeks

ReadWrite Lock
Imagine you have an application where you have multiple readers and
multiple writers. You are asked to design a lock which lets multiple readers
read at the same time, but only one writer write at a time.

To implement a reader-writer lock in Java that allows multiple readers to


access a shared resource simultaneously while ensuring exclusive access for
writers, we can utilize the wait() and notify() methods for
synchronization. This approach ensures that writers have exclusive access to
the resource, and readers can access it concurrently without interference.

Custom Reader-Writer Lock Implementation:

public class CustomReadWriteLock {


private int readers = 0; // Number of active readers
private int writers = 0; // Number of active writers
private int writeRequests = 0; // Number of write requests
private final Object lock = new Object(); // Lock object for synchronization

// Acquire read lock


public void acquireReadLock() throws InterruptedException {
synchronized (lock) {
while (writers > 0 || writeRequests > 0) {
lock.wait(); // Wait if there's an active writer or pending writ
}
readers++;
}
}
// Release read lock
public void releaseReadLock() {
synchronized (lock) {
readers--;
if (readers == 0) {
lock.notifyAll(); // Notify waiting writers
}
}
}

// Acquire write lock


public void acquireWriteLock() throws InterruptedException {
synchronized (lock) {
writeRequests++;
while (readers > 0 || writers > 0) {
lock.wait(); // Wait if there are active readers or writers
}
writeRequests--;
writers++;
}
}

// Release write lock


public void releaseWriteLock() {
synchronized (lock) {
writers--;
lock.notifyAll(); // Notify all waiting threads
}
}
}

acquireReadLock() : A thread attempting to acquire the read lock waits if


there are active writers or pending write requests. Once the conditions
are favorable, it increments the readers count.

releaseReadLock() : A thread releasing the read lock decrements the


readers count. If there are no active readers left, it notifies all waiting
threads, allowing writers to proceed.

acquireWriteLock() : A thread attempting to acquire the write lock


increments the writeRequests count and waits if there are active readers
or writers. Once the conditions are favorable, it decrements the
writeRequests count and increments the writers count.

releaseWriteLock() : A thread releasing the write lock decrements the


writers count and notifies all waiting threads, allowing either readers or
writers to proceed.

Usage Example:

public class ReaderWriterTest {


public static void main(String[] args) {
CustomReadWriteLock rwLock = new CustomReadWriteLock();

// Reader thread
Thread readerThread = new Thread(() -> {
try {
rwLock.acquireReadLock();
System.out.println("Reader thread is reading.");
Thread.sleep(1000); // Simulate reading
rwLock.releaseReadLock();
System.out.println("Reader thread has finished reading.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});

// Writer thread
Thread writerThread = new Thread(() -> {
try {
rwLock.acquireWriteLock();
System.out.println("Writer thread is writing.");
Thread.sleep(1000); // Simulate writing
rwLock.releaseWriteLock();
System.out.println("Writer thread has finished writing.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});

readerThread.start();
writerThread.start();
}
}

Expected Output:

Reader thread is reading.


Reader thread has finished reading.
Writer thread is writing.
Writer thread has finished writing.

The reader thread acquires the read lock, performs its reading task, and
then releases the read lock.

The writer thread waits for the reader to release the read lock, acquires
the write lock, performs its writing task, and then releases the write lock.

Considerations:

Starvation Prevention: This implementation ensures that writers are not


starved by waiting for readers to finish. Writers acquire the write lock
only when there are no active readers or writers.

Thread Safety: The synchronized blocks ensure that only one thread can
execute the critical sections at a time, preventing race conditions.

Fairness: This implementation does not guarantee fairness. In scenarios


with a high number of readers, writers may experience starvation. To
address this, a more sophisticated approach, such as using a fair lock,
would be necessary.

For a more in-depth understanding of read-write locks and their


implementations in Java, you might find the following resource helpful:

Read / Write Locks in Java — Jenkov.com

This resource provides a comprehensive overview of read-write locks,


including their usage and potential pitfalls.

Unisex Bathroom Problem

A synchronization practice problem requiring us to synchronize the usage of a


single bathroom by both the genders.

Designing a unisex bathroom system that allows both males and females to
use the facility while ensuring that no more than three individuals are
present simultaneously, and preventing simultaneous use by both genders,
requires careful synchronization to avoid deadlocks and starvation.

Problem Constraints:

1. The bathroom cannot be used by males and females at the same time.

2. A maximum of three employees can use the bathroom simultaneously.

3. The solution must prevent deadlocks and starvation.

Solution Approach:

We’ll implement a UnisexBathroom class that manages access to the bathroom


using semaphores and synchronization mechanisms. The class will provide
two methods: maleUseBathroom() and femaleUseBathroom() , which will be
called by male and female threads, respectively, to request access.

Key Components:

inUseBy : A variable indicating the current gender using the bathroom


( "men" , "women" , or "none" ).

currentCount : Tracks the number of individuals currently in the


bathroom.

bathroomSemaphore : A semaphore initialized to 3, limiting access to a


maximum of three individuals.

lock : A ReentrantLock to manage synchronization.

condition : A Condition object to manage threads waiting for access.

Implementation:

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class UnisexBathroom {


private String inUseBy = "none";
private int currentCount = 0;
private final Semaphore bathroomSemaphore = new Semaphore(3);
private final Lock lock = new ReentrantLock(true); // Fair lock to prevent s
private final Condition condition = lock.newCondition();

public void maleUseBathroom() throws InterruptedException {


lock.lock();
try {
while (!inUseBy.equals("none") && !inUseBy.equals("men")) {
condition.await();
}
inUseBy = "men";
bathroomSemaphore.acquire();
currentCount++;
} finally {
lock.unlock();
}

useBathroom("Male");

lock.lock();
try {
currentCount--;
bathroomSemaphore.release();
if (currentCount == 0) {
inUseBy = "none";
condition.signalAll();
}
} finally {
lock.unlock();
}
}

public void femaleUseBathroom() throws InterruptedException {


lock.lock();
try {
while (!inUseBy.equals("none") && !inUseBy.equals("women")) {
condition.await();
}
inUseBy = "women";
bathroomSemaphore.acquire();
currentCount++;
} finally {
lock.unlock();
}

useBathroom("Female");

lock.lock();
try {
currentCount--;
bathroomSemaphore.release();
if (currentCount == 0) {
inUseBy = "none";
condition.signalAll();
}
} finally {
lock.unlock();
}
}

private void useBathroom(String gender) {


System.out.println(gender + " is using the bathroom. Current count: " +
try {
Thread.sleep(1000); // Simulate bathroom usage
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(gender + " is leaving the bathroom.");
}
}

Fair Lock ( ReentrantLock(true) ): Using a fair lock ensures that threads


acquire the lock in the order they requested it, preventing starvation.
Gender Check: Before entering, a thread checks if the bathroom is either
empty or currently occupied by the same gender. If not, it waits.

Semaphore: Controls the maximum number of individuals in the


bathroom, ensuring it doesn’t exceed three.

State Management: The inUseBy variable tracks which gender is


currently using the bathroom. When the bathroom becomes empty
( currentCount == 0 ), inUseBy is set to "none" , allowing the opposite
gender to enter.

Condition Signaling: When the bathroom becomes empty, all waiting


threads are notified to re-evaluate the conditions, ensuring fair access.

Usage Example:

public class BathroomSimulation {


public static void main(String[] args) {
UnisexBathroom bathroom = new UnisexBathroom();

Runnable maleTask = () -> {


try {
bathroom.maleUseBathroom();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};

Runnable femaleTask = () -> {


try {
bathroom.femaleUseBathroom();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};

// Create and start threads for simulation


Thread[] males = new Thread[5];
Thread[] females = new Thread[5];

for (int i = 0; i < 5; i++) {


males[i] = new Thread(maleTask, "Male-" + (i + 1));
females[i] = new Thread(femaleTask, "Female-" + (i + 1));
}

for (int i = 0; i < 5; i++) {


males[i].start();
females[i].start();
}

// Wait for all threads to finish


for (int i = 0; i < 5; i++) {
try {
males[i].join();
females[i].join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}

Considerations:

Deadlock Prevention: The use of ReentrantLock and Condition variables


ensures that threads wait and notify appropriately, preventing deadlocks.

Starvation Prevention: The fair lock ( ReentrantLock(true) ) ensures that


threads are granted access in the order they arrive, preventing starvation
of any gender.

Concurrency: The semaphore limits the number of concurrent users to


three, adhering to the problem constraints.

This implementation ensures that the bathroom is used safely and efficiently
by multiple threads, adhering to the specified constraints and preventing
both deadlocks and starvation.

Implementing a Barrier
A barrier can be thought of as a point in the program code, which all or some of
the threads need to reach at before any one of them is allowed to proceed
further.

A barrier allows multiple threads to congregate at a point in code before any


one of the threads is allowed to move forward. Java and most other
languages provide libraries which make barrier construct available for
developer use.

A CyclicBarrier in Java is a synchronization aid that allows a set of threads to


wait for each other to reach a common barrier point. Once all threads have
reached this point, they can proceed with their execution. This mechanism
is particularly useful in scenarios where multiple threads must wait for each
other to reach a certain state before continuing.

Key Characteristics of CyclicBarrier:

Reusability: Unlike CountDownLatch , a CyclicBarrier can be reused after


the waiting threads are released, making it suitable for iterative
processes.
Parties: The number of threads that must invoke the await() method
before any of them can proceed.

Barrier Action: An optional Runnable that is executed once the last thread
arrives at the barrier.

Implementing a Custom CyclicBarrier:

While Java provides a built-in CyclicBarrier class in the


java.util.concurrent package, understanding how to implement one can
deepen your grasp of synchronization mechanisms. Below is a detailed
implementation of a custom CyclicBarrier :

public class CustomCyclicBarrier {


private final int initialParties;
private int partiesAwait;
private Runnable barrierAction;

public CustomCyclicBarrier(int parties, Runnable barrierAction) {


if (parties <= 0) throw new IllegalArgumentException("Number of parties
this.initialParties = parties;
this.partiesAwait = parties;
this.barrierAction = barrierAction;
}

public CustomCyclicBarrier(int parties) {


this(parties, null);
}

public synchronized void await() throws InterruptedException {


partiesAwait--;

if (partiesAwait > 0) {
this.wait();
} else {
// All parties have arrived
partiesAwait = initialParties; // Reset for reuse
if (barrierAction != null) {
barrierAction.run();
}
notifyAll();
}
}
}

Constructor: Initializes the barrier with the specified number of parties


and an optional barrier action.

await(): Decrements the partiesAwait count. If not all parties have


arrived, the thread waits. When the last thread arrives, it resets the
barrier, executes the barrier action if provided, and notifies all waiting
threads.
Usage Example:

public class BarrierDemo {


public static void main(String[] args) {
int numberOfThreads = 3;
CustomCyclicBarrier barrier = new CustomCyclicBarrier(numberOfThreads, (
System.out.println("All parties have arrived at the barrier. Executi
});

for (int i = 0; i < numberOfThreads; i++) {


new Thread(new Task(barrier)).start();
}
}
}

class Task implements Runnable {


private CustomCyclicBarrier barrier;

public Task(CustomCyclicBarrier barrier) {


this.barrier = barrier;
}

@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " is performin
// Simulate work
Thread.sleep((long) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + " is waiting a
barrier.await();
System.out.println(Thread.currentThread().getName() + " has crossed
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

Output:

Thread-0 is performing work.


Thread-1 is performing work.
Thread-2 is performing work.
Thread-1 is waiting at the barrier.
Thread-0 is waiting at the barrier.
Thread-2 is waiting at the barrier.
All parties have arrived at the barrier. Executing barrier action.
Thread-1 has crossed the barrier.
Thread-0 has crossed the barrier.
Thread-2 has crossed the barrier.

Considerations:

Thread Safety: The await() method is synchronized to ensure thread


safety.
Reusability: The barrier resets after all threads have reached it, allowing
for reuse in cyclic operations.

Barrier Action: An optional action that executes once all threads have
reached the barrier.

Implementing a custom CyclicBarrier provides insight into synchronization


mechanisms, but in production scenarios, it's advisable to use Java's built-in
CyclicBarrier for robustness and maintainability.

For more detailed information on CyclicBarrier , refer to the official Java


documentation.

Uber Ride Problem


Imagine at the end of a political conference, republicans and democrats are
trying to leave the venue and ordering Uber rides at the same time. However,
to make sure no fight breaks out in an Uber ride, the software developers at
Uber come up with an algorithm whereby either an Uber ride can have all
democrats or republicans or two Democrats and two Republicans. All other
combinations can result in a fist-fight.
In Java, semaphores and locks are essential tools for managing concurrency,
ensuring that multiple threads can operate without interfering with each
other. Below are detailed Java programs demonstrating the use of
semaphores and reentrant locks in various scenarios.

1. Semaphore Example: Limiting Access to a Shared Resource

A semaphore controls access to a shared resource through a counter,


allowing a specified number of threads to access the resource concurrently.
GeeksforGeeks

import java.util.concurrent.Semaphore;

public class SemaphoreExample {


// Semaphore with 3 permits, allowing up to 3 threads to access the resource
private static final Semaphore semaphore = new Semaphore(3);

public static void main(String[] args) {


// Creating and starting 5 threads
for (int i = 0; i < 5; i++) {
new Thread(new Worker(i)).start();
}
}

static class Worker implements Runnable {


private final int workerId;

Worker(int workerId) {
this.workerId = workerId;
}

@Override
public void run() {
try {
// Acquiring a permit before accessing the shared resource
semaphore.acquire();
System.out.println("Worker " + workerId + " is accessing the res
// Simulating work by sleeping for 2 seconds
Thread.sleep(2000);
System.out.println("Worker " + workerId + " is releasing the res
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// Releasing the permit after accessing the resource
semaphore.release();
}
}
}
}

The Semaphore is initialized with 3 permits, allowing up to 3 threads to


access the shared resource concurrently.

Each Worker thread attempts to acquire a permit before accessing the


resource. If permits are unavailable, the thread blocks until one becomes
available.

After completing its work, the thread releases the permit, allowing other
threads to acquire it.

2. ReentrantLock Example: Coordinating Access to Shared Resources

The ReentrantLock class provides explicit lock mechanisms, offering greater


flexibility than the synchronized keyword. It allows a thread to acquire the
same lock multiple times and includes features like fairness policies and the
ability to interrupt lock acquisition. GeeksforGeeks

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {


// ReentrantLock instance
private static final ReentrantLock lock = new ReentrantLock();
private static int sharedCounter = 0;

public static void main(String[] args) {


// Creating and starting 3 threads
for (int i = 0; i < 3; i++) {
new Thread(new CounterIncrementer()).start();
}
}
static class CounterIncrementer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
lock.lock(); // Acquiring the lock
try {
// Incrementing the shared counter
sharedCounter++;
System.out.println(Thread.currentThread().getName() + " incr
} finally {
lock.unlock(); // Releasing the lock
}
try {
// Simulating work by sleeping for 1 second
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
}

The ReentrantLock ensures that only one thread at a time can execute the
critical section where the shared counter is incremented.

Each thread acquires the lock before modifying the shared counter and
releases it afterward, ensuring thread-safe operations.

3. Barrier Example: Synchronizing Threads at a Common Point

A barrier allows multiple threads to wait for each other at a specific point
before proceeding. This ensures that threads reach a particular execution
point before any can continue, facilitating coordinated behavior.

import java.util.concurrent.CyclicBarrier;

public class BarrierExample {


// CyclicBarrier that waits for 3 threads
private static final CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All parties have arrived at the barrier, let's proce
});

public static void main(String[] args) {


// Creating and starting 3 threads
for (int i = 0; i < 3; i++) {
new Thread(new Task(i)).start();
}
}

static class Task implements Runnable {


private final int taskId;

Task(int taskId) {
this.taskId = taskId;
}

@Override
public void run() {
try {
System.out.println("Task " + taskId + " is performing work.");
// Simulating work by sleeping for a random time
Thread.sleep((long) (Math.random() * 3000));
System.out.println("Task " + taskId + " is waiting at the barrie
// Waiting at the barrier
barrier.await();
System.out.println("Task " + taskId + " has crossed the barrier.
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}
}
}

The CyclicBarrier is initialized to wait for 3 threads. Once all threads


reach the barrier, the provided Runnable is executed, and all threads are
released to continue their execution.

Each Task thread performs some work, waits at the barrier, and then
proceeds once all threads have reached the barrier.

4. Uber Ride Problem: Synchronizing Threads Based on Conditions

This problem involves synchronizing threads representing Democrats and


Republicans to ensure that Uber rides are composed of either all members
of the same party or an equal number from both parties.

import java.util.concurrent.Semaphore;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.locks.ReentrantLock;

public class UberRide {


private int democrats = 0;
private int republicans = 0;
private final ReentrantLock lock = new ReentrantLock();
private final Semaphore demsWaiting = new Semaphore(0);
private final Semaphore repubsWaiting = new Semaphore(0);
private final CyclicBarrier barrier = new CyclicBarrier(4);

public void seatDemocrat() throws InterruptedException, BrokenBarrierExcepti


boolean rideLeader = false;
lock.lock();
try {
democrats++;
if (democrats == 4) {
demsWaiting.release(3);
democrats -= 4;
rideLeader = true;
} else if (democrats == 2 && republicans >= 2) {
demsWaiting.release(1);
repubsWaiting.release(2);
democrats -= 2;
republicans -= 2;
rideLeader = true;
} else {
lock.unlock();
demsWaiting.acquire();
return;
}
} finally {
if (!rideLeader) {
lock.unlock();
}
}
seated();
barrier.await();
if (rideLeader) {
drive();
lock.unlock();
}
}

public void seatRepublican() throws InterruptedException, BrokenBarrierExcep


boolean rideLeader = false;
lock.lock();
try {
republicans++;
if (republicans == 4) {
repubsWaiting.release(3);
republicans -= 4;
rideLeader = true;
} else if (republicans == 2 && democrats >= 2) {
repubsWaiting.release(1);
demsWaiting.release(2);
republicans -= 2;
democrats -= 2;
rideLeader = true;
} else {
lock.unlock();
repubsWaiting.acquire();
return;
}
} finally {
if (!rideLeader) {
lock.unlock();
}
}
seated();
barrier.await();
if (rideLeader) {
drive();
lock.unlock();
}
}

private void seated() {


// Simulate the action of being seated
System.out.println(Thread.currentThread().getName() + " is seated.");
}

private void drive() {


// Simulate the action of starting the ride
System.out.println("Ride is starting with 4 passengers.");
}
}

To address the Uber Ride Problem, we need to ensure that threads


representing Democrats and Republicans are coordinated such that each
Uber ride contains either four passengers of the same party or two from
each party. This coordination prevents conflicts and ensures safe rides.

Implementation Overview:

Synchronization Primitives:

ReentrantLock: Used to manage access to shared resources, ensuring


that only one thread can modify the counts of waiting Democrats and
Republicans at a time.

Semaphores:

demsWaiting : Controls the number of Democrat threads allowed to


proceed. Initialized to 0, meaning threads will block until released.

repubsWaiting : Controls the number of Republican threads allowed to


proceed. Also initialized to 0.

CyclicBarrier: Ensures that exactly four threads (passengers) are


synchronized to simulate all passengers being seated before the ride
starts.

Shared Counters:

democrats : Tracks the number of Democrat threads waiting for a ride.

republicans : Tracks the number of Republican threads waiting for a ride.

Methods:

seatDemocrat() : Called by Democrat threads to attempt to get a seat.

seatRepublican() : Called by Republican threads to attempt to get a seat.

seated() : Simulates a passenger being seated.

drive() : Called once all four passengers are seated to start the ride.

Locking Mechanism: Each thread acquires the lock to ensure exclusive


access when modifying the democrats and republicans counters. This
prevents race conditions.

Semaphore Usage:
Threads that cannot form a valid group immediately release the lock and
acquire the respective semaphore ( demsWaiting or repubsWaiting ),

causing them to wait until enough threads arrive to form a valid group.

When a valid group is formed (either four of the same party or two of
each), the leading thread releases the appropriate number of waiting
threads by calling release() on the semaphores.

CyclicBarrier: Once a thread is seated, it calls barrier.await() , causing it


to wait until all four threads have reached this point. This ensures that all
passengers are seated before the ride starts.

Ride Leader: The thread that forms a valid group becomes the ride
leader, responsible for calling drive() to start the ride. This thread also
ensures the lock is released after starting the ride.

Testing the Implementation:

To test the implementation of the Uber ride problem, we can create a


simulation where multiple threads represent Democrats and Republicans
requesting rides concurrently. The goal is to ensure that the synchronization
logic correctly groups riders into acceptable combinations (either all
Democrats, all Republicans, or two Democrats and two Republicans) without
causing deadlocks or starvation.

Here’s how we can set up the test:

1. Define the UberRide Class: This class will contain the synchronization
logic as previously discussed, including methods for Democrats and
Republicans to request seats, and the drive() method to start the ride.

2. Create Rider Threads: We’ll define a Rider class that extends Thread .

Each Rider will have a party affiliation ("Democrat" or "Republican") and


will attempt to get a seat by calling the appropriate method in the
UberRide class.

3. Simulate Multiple Ride Requests: In the main method, we'll create


multiple Rider threads with varying party affiliations and start them
concurrently to simulate the ride-requesting process.

4. Monitor the Output: Each rider will print messages when seated and
when the ride starts. This output will help us verify that the riders are
grouped correctly and that the system behaves as expected under
concurrent conditions.

Here’s the complete implementation:


import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class UberRide {


private int democrats = 0;
private int republicans = 0;
private final Lock lock = new ReentrantLock();
private final Semaphore demsWaiting = new Semaphore(0);
private final Semaphore repubsWaiting = new Semaphore(0);
private final Semaphore rideLeader = new Semaphore(0);

public void seatDemocrat() throws InterruptedException {


boolean leader = false;
lock.lock();
try {
democrats++;
if (democrats == 4) {
demsWaiting.release(3);
democrats -= 4;
leader = true;
} else if (democrats == 2 && republicans >= 2) {
demsWaiting.release(1);
repubsWaiting.release(2);
democrats -= 2;
republicans -= 2;
leader = true;
} else {
lock.unlock();
demsWaiting.acquire();
return;
}
} finally {
if (!leader) {
lock.unlock();
}
}
seated();
rideLeader.acquire();
if (leader) {
drive();
lock.unlock();
}
}

public void seatRepublican() throws InterruptedException {


boolean leader = false;
lock.lock();
try {
republicans++;
if (republicans == 4) {
repubsWaiting.release(3);
republicans -= 4;
leader = true;
} else if (republicans == 2 && democrats >= 2) {
repubsWaiting.release(1);
demsWaiting.release(2);
republicans -= 2;
democrats -= 2;
leader = true;
} else {
lock.unlock();
repubsWaiting.acquire();
return;
}
} finally {
if (!leader) {
lock.unlock();
}
}
seated();
rideLeader.acquire();
if (leader) {
drive();
lock.unlock();
}
}

private void seated() throws InterruptedException {


System.out.println(Thread.currentThread().getName() + " is seated.");
rideLeader.release();
}

private void drive() {


System.out.println("Ride is starting with " + Thread.currentThread().get
}

public static void main(String[] args) {


UberRide uberRide = new UberRide();

class Rider extends Thread {


private final String party;

Rider(String party) {
this.party = party;
}

@Override
public void run() {
try {
if ("Democrat".equals(party)) {
uberRide.seatDemocrat();
} else {
uberRide.seatRepublican();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

// Create and start rider threads


String[] parties = {"Democrat", "Republican", "Democrat", "Republican",
for (String party : parties) {
new Rider(party).start();
}
}
}

UberRide Class: Manages the counts of waiting Democrats and


Republicans, and uses semaphores to control the seating process. The
seatDemocrat() and seatRepublican() methods handle the logic for
seating riders based on the current counts and permissible
combinations.

Rider Class: Represents a rider thread with a specified party affiliation.


Each rider attempts to get a seat by calling the appropriate method in the
UberRide class.

Main Method: Initializes the UberRide instance and creates multiple


Rider threads with alternating party affiliations to simulate concurrent
ride requests.

Output:

The program will print messages indicating when each rider is seated and
when a ride starts. For example:

Thread-0 is seated.
Thread-1 is seated.
Thread-2 is seated.
Thread-3 is seated.
Ride is starting with Thread-0 as the leader.
Thread-4 is seated.
Thread-5 is seated.
Thread-6 is seated.
Thread-7 is seated.
Ride is starting with Thread-4 as the leader.

This output demonstrates that riders are grouped into acceptable


combinations, and rides start as expected without any deadlocks or
starvation.

Note: The order of thread execution may vary due to the concurrent nature
of threads. The provided implementation ensures that the synchronization
constraints are met, and acceptable rider combinations are formed for each
ride.

Java Interview Programming Multithreading Engineering

Written by yugal-nandurkar Follow


16 Followers · 1 Following

https://github.com/yugal-nandurkar || https://www.linkedin.com/in/yugal-
nandurkar/ || https://medium.com/@microteam93

No responses yet

Write a response
What are your thoughts?

More from yugal-nandurkar

yugal-nandurkar yugal-nandurkar

Spring Boot (Baeldung Java Multithreading for Senior


Perspective) Engineering Interviews (Part I)
Exploring Spring Boot Why threads exist and what benefit do they
provide?

Feb 7 Jan 8 1

yugal-nandurkar yugal-nandurkar

Digital Twin in Next-Generation Portfolio Skillset (Spring Boot


Computer Networks (Part I) Developer)
This project develops a digital twin for next- Portfolio Skillset (Java Developer) | by yugal-
generation computer networks through a… nandurkar | Feb, 2025 | Medium

Feb 23 Feb 7

See all from yugal-nandurkar


Recommended from Medium

AKCoding.com Sujith C

Most Asked Data Structure & Understanding CAP Theorem in


Algorithm Questions in Interviews System Design with a Practical…
Not a Premium Medium member? Click here In the world of distributed systems, the CAP
to access it for free! theorem serves as a foundational concept fo…

Feb 20 6d ago

Byte Wise 010 Ajay Rathod

Centralized Monitoring with Spring Top 15 DS Algo Interview Questions


Boot Admin for Java Developers(Commonly…
From Zero to Production-Ready Observability Hello guys, if you are you a software engineer
in 15 Minutes or specifically a Java Developer and you hav…

5d ago 5d ago

Ramesh Fadatare In Stackademic by Lets Learn Now

REST API Design for Long-Running From Zero to Hero in Kafka:


Tasks 🚀 Understanding the Core Concepts
Some operations in REST APIs take a long Bootstrap Server
time to complete, such as processing large…
Mar 13 Mar 11

See more recommendations

Help Status About Careers Press Blog Privacy Rules Terms Text to speech

You might also like