Understanding Monitor Interaction
Thread synchronization in Java relies heavily on the intrinsic lock associated with every object. When a thread enters a synchronized block, it acquires the monitor. If conditions within that critical section are not yet met, the thread can voluntarily release the monitor and pause execution using wait().
Mechanism Workflow
- Release Lock: A thread holding the monitor detects a condition that prevents immediate progress. It calls
wait(), releasing the lock and transitioning its state toWAITING. - Monitor Handoff: Once the lock is released, blocked threads waiting in the EntryList are woken up to compete for ownership.
- Re-entrance: Upon being notified, a thread does not immediately resume execution. It re-acquires the monitor from the current owner or competes for it before resuming.
- Notification: Threads can signal others to check their conditions again via
notify()(random selection) ornotifyAll()(all waiting threads).
Core APIs
These methods belong to the Object class, meaning any instance can serve as a monitor. Crucially, they require thread synchronization. Invoking them outside a synchronized context throws IllegalMonitorStateException.
obj.wait(); // Blocks indefinitely until notified
obj.wait(long); // Blocks for a specified duration
obj.notify(); // Wakes up one waiting thread
obj.notifyAll(); // Wakes up all waiting threads
Practical Demonstration
Attempting to call wait() without holding the object's lock results in an exception.
public class MonitorUsage {
public static void main(String[] args) {
Object lock = new Object();
new Thread(() -> {
synchronized (lock) {
System.out.println("Thread acquired lock");
try {
lock.wait();
System.out.println("Thread resumed");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Worker").start();
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {}
synchronized (lock) {
lock.notify();
}
}
}
wait() vs sleep()
| Feature | wait() | sleep() |
|---|---|---|
| Class | Object method |
Thread static method |
| Lock Handling | Releases the monitor | Retains the monitor |
| Requirement | Must be called inside synchronized |
No special requirement |
| State | TIME_WAITING (waiting for notify) |
TIMED_WAITING (time elapsed) |
Safeguarding Against Spurious Wakeups
A "spurious wakeup" occurs when a thread resumes execution with out an explicit notify() call, though rare, relying on it causes logical errors. To mitigate this, always wrap the condition check in a while loop rather than an if statement.
synchronized (lock) {
while (!conditionMet()) {
lock.wait();
}
// Proceed with task
}
// Signaler
synchronized (lock) {
notifyAll();
}
Guarded Suspension Pattern
This pattern addresses scenarios where one thread must suspend execution until another thread produces data. It effectively decouples the waiter from the producer.
Implementation Example
A wrapper class manages the result availability. The consumer waits, while the producer signals completion.
class ResultGuard {
private volatile String data;
public synchronized Object get() throws InterruptedException {
while (data == null) {
wait();
}
return data;
}
public synchronized void set(String newData) {
data = newData;
notifyAll();
}
}
Timed Waiting Support
Sometimes a task cannot hang indefinitely. We modify the guard to track elapsed time during the wait loop.
class TimeLimitedGuard {
private String payload;
public synchronized Object get(long timeoutMillis) throws InterruptedException {
long deadline = System.currentTimeMillis() + timeoutMillis;
while (payload == null) {
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0) return null;
wait(remaining);
}
return payload;
}
public synchronized void complete(String val) {
payload = val;
notifyAll();
}
}
Integration with Thread.join()
The standard Thread.join() method internally implements a form of guarded suspension. It loops while the target thread is alive, invoking wait(0) to pause execution until the other thread terminates.
Decoupled Management: The Mailbox Approach
Managing individual guards for many pairs becomes cumbersome. A centralized mailbox system allows multiple producers to send to multiple consumers dynamically.
Architecture:
- Unique ID assigned to each request.
- Central manager holds a registry of active requests.
- Producers claim IDs, Consumers retrieve objects by ID.
import java.util.concurrent.ConcurrentHashMap;
import java.util.Set;
class MailboxRegistry {
private ConcurrentHashMap<Integer, Object> cache = new ConcurrentHashMap<>();
private int idCounter = 0;
public synchronized Integer createRequest() {
return ++idCounter;
}
public synchronized void deliver(Integer id, Object content) {
// Simulated storage logic with notification would go here
// Using a separate GuardedObject per ID is typical in this pattern
}
}
Producer-Consumer Pattern
Unlike guarded suspension which is typically synchronous and 1-to-1, this pattern uses a buffer (queue) to allow asynchronous production and consumption. Resources are balanced via queue capacity limits.
Design Considerations
- Blocking: Producers wait if the queue is full; Consumers wait if the queue is empty.
- Capacity: A fixed size prevents unbounded memory growth.
- Decoupling: Producers don't know who consumes the data.
Custom Blocking Channel
import java.util.LinkedList;
import java.util.Queue;
class BlockingChannel<T> {
private final Queue<T> buffer;
private final int maxSize;
public BlockingChannel(int size) {
this.maxSize = size;
this.buffer = new LinkedList<>();
}
public synchronized T take() throws InterruptedException {
while (buffer.isEmpty()) {
wait();
}
T item = buffer.removeFirst();
notifyAll(); // Alert producers that space is available
return item;
}
public synchronized void put(T item) throws InterruptedException {
while (buffer.size() == maxSize) {
wait();
}
buffer.addLast(item);
notifyAll(); // Alert consumers that data is available
}
}
Execution Flow
Multiple producer threads add data to the channel. A single consumer thread retrieves and processes items. If the buffer reaches capacity, further putss block. If the buffer empties, gets block until new data arrives.
public class ChannelDemo {
public static void main(String[] args) throws Exception {
BlockingChannel<String> queue = new BlockingChannel<>(5);
for (int i = 0; i < 10; i++) {
final int index = i;
new Thread(() -> {
try {
queue.put("Item-" + index);
System.out.println("Produced: Item-" + index);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
new Thread(() -> {
try {
while (true) {
String item = queue.take();
System.out.println("Consumed: " + item);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(10000);
}
}
This implementation demonstrates how wait and notify form the foundation of thread coordination, allowing developers to build complex synchronization structures beyond basic locking.