Java Thread Synchronization: Wait/Notify, Condition, and LockSupport

Java provides several mechanisms for managing thread coordination. Understanding these methods and their nuances is crucial for building robust multithreaded applications.

1. Object.wait() and Object.notify()

This pair of methods allows a thread to yield its execution until another thread notifies it. Both wait() and notify() (or notifyAll()) must be invoked with in a synchronized block or method that synchronizes on the same object.

Correct Usage:

A thread calls wait() on an object, releasing the lock and entering a waiting state. Another thread acquires the lock, performs some operation, and then calls notify() to wake up a waiting thread.

import java.util.concurrent.TimeUnit;

public class WaitNotifyDemo {

    public static void main(String[] args) {
        final Object lock = new Object();

        Thread waitingThread = new Thread(() -> {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "\t ---- entered and waiting");
                try {
                    lock.wait(); // Releases the lock and waits
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // Restore interrupted status
                    System.err.println("Waiting thread interrupted.");
                }
                System.out.println(Thread.currentThread().getName() + "\t ---- woken up");
            }
        }, "Waiter");

        waitingThread.start();

        // Give the waiting thread time to start and wait
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        Thread notifyingThread = new Thread(() -> {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "\t ---- acquired lock and notifying");
                lock.notify(); // Wakes up one waiting thread
            }
        }, "Notifier");

        notifyingThread.start();
    }
}

Common Pitfalls:

  • Calling wait() or notify() without holding the lock: This will result in an IllegalMonitorStateException. Both operations require the calling thread to own the intrinsic lock of the object.

    // Incorrect: Missing synchronized block
    // object.wait();
    // object.notify();
    
  • Incorrect timing of wait() and notify(): If notify() is called before wait(), the notification might be lost, and the waiting thread may never wake up (unless another notification occurs later).

    // Example scenario where notify might happen before wait
    Thread notifyingThread = new Thread(() -> {
        synchronized (lock) {
            lock.notify(); // Called before wait()
            System.out.println(Thread.currentThread().getName() + "\t ---- sent notification");
        }
    }, "Notifier");
    notifyingThread.start();
    
    // ... later, waitingThread calls wait() ...
    

In summary, wait() and notify() must be used within a synchronized context on the same object, and the sequence typically involves wait() being called before notify() to ensure proper coordination.

2. Condition.await() and Condition.signal()

The java.util.concurrent.locks.Condition interface, often used with ReentrantLock, provides a more flexible alternative to Object.wait() and Object.notify().

Correct Usage:

A Condition object is obtained from a Lock. Threads must acquire the lock before calling await() or signal() on the condition. await() releases the lock associated with the condition and waits, while signal() (or signalAll()) wakes up one or all waiting threads, respectively. The lock must be released by the thread after the operation.

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

public class ConditionDemo {

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        Thread waitingThread = new Thread(() -> {
            lock.lock(); // Acquire the lock
            try {
                System.out.println(Thread.currentThread().getName() + "\t ---- entered and waiting");
                condition.await(); // Releases lock and waits
                System.out.println(Thread.currentThread().getName() + "\t ---- woken up");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println("Waiting thread interrupted.");
            } finally {
                lock.unlock(); // Release the lock
            }
        }, "Waiter");

        waitingThread.start();

        // Give the waiting thread time to start and wait
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        Thread notifyingThread = new Thread(() -> {
            lock.lock(); // Acquire the lock
            try {
                System.out.println(Thread.currentThread().getName() + "\t ---- acquired lock and signaling");
                condition.signal(); // Wakes up one waiting thread
            } finally {
                lock.unlock(); // Release the lock
            }
        }, "Notifier");

        notifyingThread.start();
    }
}

Common Pitfalls:

  • Calling await() or signal() without holding the associated lock: Similar to wait/notify, these operations require the thread to hold the lock.

    // Incorrect: Missing lock acquisition
    // condition.await();
    // condition.signal();
    
  • Incorrect execution order: The thread must acquire the lock, call await(), and then another thread must acquire the same lock and call signal(). If the signal occurs before the await, the waiting thread might miss the signal.

    // Example scenario with reversed order (requires careful synchronization)
    Thread waitingThread = new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(1); // Delays the await
        } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t ---- entered and waiting");
            condition.await(); // Might miss the signal if it arrived early
            System.out.println(Thread.currentThread().getName() + "\t ---- woken up");
        } finally { lock.unlock(); }
    }, "Waiter");
    waitingThread.start();
    
    Thread notifyingThread = new Thread(() -> {
        lock.lock();
        try {
            condition.signal(); // Signal sent early
            System.out.println(Thread.currentThread().getName() + "\t ---- sent signal");
        } finally { lock.unlock(); }
    }, "Notifier");
    notifyingThread.start();
    

The Condition interface requires operations to be performed within the lock's scope, and typically, a thread waits before another signals it.

3. LockSupport.park() and LockSupport.unpark()

The LockSupport class offers a lower-level, more flexible mechanism for thread suspension and resumption. It does not require explicit locks.

Correct Usage:

LockSupport.park() suspends the current thread until it receives a permit. LockSupport.unpark(thread) grants a permit to the specified thread, allowing it to proceed if it's parked, or making its next park() call return immediately.

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class LockSupportDemo {

    public static void main(String[] args) {
        Thread waitingThread = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t ---- entered and parked");
            LockSupport.park(); // Waits for a permit
            System.out.println(Thread.currentThread().getName() + "\t ---- unparked and resumed");
        }, "Parker");

        waitingThread.start();

        // Give the parker thread time to start and park
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // Unpark the waiting thread
        LockSupport.unpark(waitingThread);
        System.out.println(Thread.currentThread().getName() + "\t ---- unparked Parker");
    }
}

Flexibility:

  • No Lock Requirement: park() and unpark() do not require synchronization on any object.

  • Order Independence: unpark() can be called before park(). If called first, the subsequent park() will return immediately as if a permit was already available.

    Thread waitingThread = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "\t ---- entered and parked");
        // If unpark was called first, this park() returns immediately
        LockSupport.park(); 
        System.out.println(Thread.currentThread().getName() + "\t ---- unparked and resumed");
    }, "Parker");
    waitingThread.start();
    
    // Unpark first
    LockSupport.unpark(waitingThread);
    System.out.println(Thread.currentThread().getName() + "\t ---- unparked Parker");
    
    // Wait a bit to show the parker thread might already be running
    try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    

Permit Limit:

  • LockSupport uses a concept of a permit, with a internal counter. The maximum value of this counter is 1. Calling unpark() multiple times consecutively with out intervening park() calls does not increase the permit count beyond 1.

    Thread parker = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "\t ---- entered and parked");
        LockSupport.park(); // First park consumes a permit
        System.out.println(Thread.currentThread().getName() + "\t ---- resumed after first park");
        LockSupport.park(); // This will block as the permit is already used (or if never unparked)
        System.out.println(Thread.currentThread().getName() + "\t ---- resumed after second park");
    }, "Parker");
    parker.start();
    
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    
    System.out.println(Thread.currentThread().getName() + "\t ---- sending first unpark");
    LockSupport.unpark(parker);
    // Send a second unpark, but it won't grant an extra permit beyond the initial one
    System.out.println(Thread.currentThread().getName() + "\t ---- sending second unpark");
    LockSupport.unpark(parker);
    

This behavior means multiple unpark() calls might be needed if multiple park() calls are intended, but only one unpark is effectively needed to satisfy one park when the permit count is 0.

Tags: java Concurrency multithreading Wait/Notify Condition

Posted on Sun, 05 Jul 2026 17:29:51 +0000 by greenhorn666