Keywords: Java Multithreading | Thread Synchronization | wait/notify | CountDownLatch | Condition Interface
Abstract: This article explores elegant implementations of "block until condition becomes true" in Java multithreading. Analyzing the drawbacks of polling approaches, it focuses on synchronization mechanisms using Object.wait()/notify(), with supplementary coverage of CountDownLatch and Condition interfaces. Key technical details for avoiding lost notifications and spurious wakeups are explained, accompanied by complete code examples and best practices for writing efficient and reliable concurrent programs.
Drawbacks of Polling and the Need for Improvement
In Java multithreading, a common requirement is having a thread wait until a condition becomes true before proceeding. Beginners often use polling approaches like:
while (true) {
try {
if (condition) {
// perform action
condition = false;
}
Thread.sleep(1000);
} catch (InterruptedException ex) {
Logger.getLogger(server.class.getName()).log(Level.SEVERE, null, ex);
}
}
While simple, this method has clear issues: even with sleep() reducing CPU usage, it remains inefficient busy-waiting, wasting system resources and introducing response delays. When the condition becomes true during sleep, the thread cannot respond immediately but must wait for sleep to complete.
Synchronization Mechanism Using wait/notify
Java provides a more elegant solution: using the wait() and notify() methods of the Object class. These methods must be called within synchronized blocks to ensure thread safety.
Implementation for the waiting thread (consumer):
try {
synchronized(syncObject) {
while (!conditionIsTrue()) {
syncObject.wait();
}
doSomethingThatRequiresConditionToBeTrue();
}
} catch (InterruptedException e) {
handleInterruption();
}
Implementation for the notifying thread (producer):
synchronized(syncObject) {
doSomethingThatMakesConditionTrue();
syncObject.notify();
}
Several key technical points:
- Synchronization Protection: All checks and modifications of shared conditions must occur within synchronized blocks on the same object to prevent race conditions.
- Loop Checking: wait() calls must be placed in while loops, not if statements. This is due to the possibility of "spurious wakeups"—wait() may return without a notify() call. The loop ensures revalidation of the condition.
- Avoiding Lost Notifications: If the producer calls notify() before the consumer calls wait(), the notification is lost. By placing condition checks and wait() within the same synchronized block, the consumer checks the condition before waiting; if already true, no wait is needed.
Advanced Synchronization Tools
Beyond basic wait/notify, Java's concurrency package offers richer tools.
CountDownLatch is suitable for one-time event notification:
public class EventWaiter {
private final CountDownLatch latch = new CountDownLatch(1);
public void waitForEvent() throws InterruptedException {
latch.await();
performAction();
}
public void signalEvent() {
latch.countDown();
}
}
CountDownLatch is initialized with a count; await() blocks until the count reaches zero, and countDown() decrements the count. It simplifies scenarios requiring waiting for a single event.
Lock and Condition provide more flexible synchronization control:
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void waitForCondition() throws InterruptedException {
lock.lock();
try {
while (!conditionMet()) {
condition.await();
}
executeTask();
} finally {
lock.unlock();
}
}
public void signalCondition() {
lock.lock();
try {
makeConditionTrue();
condition.signal();
} finally {
lock.unlock();
}
}
Compared to synchronized, the Lock interface supports try-lock, timed waits, and fairness policies. Condition allows multiple wait conditions per lock, better suited for complex synchronization scenarios.
Best Practices for Interrupt Handling
Proper handling of InterruptedException is crucial. It should not be merely logged and ignored; instead, restore the interrupt status or propagate the exception:
try {
syncObject.wait();
} catch (InterruptedException e) {
// Restore interrupt status so callers can detect it
Thread.currentThread().interrupt();
// Or rethrow the exception
throw e;
}
Ignoring interrupts may prevent threads from terminating normally, affecting program shutdown processes.
Guidelines for Choosing Solutions
- Simple Condition Waiting: Use synchronized with wait/notify for concise code.
- One-Time Events: CountDownLatch is the best choice.
- Complex or Multiple Conditions: Use Lock and Condition.
- Timeout Requirements: Consider wait(long timeout) or Condition.await(long time, TimeUnit unit).
Regardless of the chosen approach, ensure: 1) condition checks and waits are in loops; 2) shared state is protected by synchronization; 3) interrupts are handled properly.