1. Overview

While Lock offers an alternative to the synchronized methods, Condition offers an alternative to the Object monitor methods like wait, notify, and notifyAll. In essence, Condition allows threads to wait for some condition to become true, due to some activity happening on other threads. In this tutorial, we're going to investigate how we can use a Condition.

2. Condition Usage

2.1. Create Condition using newCondition

Let's start with creating a Condition instance.

When we acquire an intrinsic lock through the synchronized keyword, we use the monitor methods of the lock object - not some other object. In the same manner, a Condition is bound to a Lock. We can only create a Condition using an existing Lock:

public class ConditionDetails {

    private final Lock listLock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
}

Here, we're initializing a Lock instance variable - listLock. Then, we're invoking the newCondition method to create a Condition instance. Since every invocation returns a new instance, we're also storing the returned Condition in an instance variable - notEmpty.

2.2. await and signalAll

Now that we've created a Condition instance, let's put it to work.

We generally call await after noticing that a condition doesn't hold:

public void awaitOnCondition() throws InterruptedException {
    listLock.lock();
    try {
        while (isEmpty()) {
            System.out.println("I will wait now");
            notEmpty.await();
        }
    
        // Do work.
    } finally {
        listLock.unlock();
    }
}

Here, we're first acquiring the lock. Because if we invoke the await method without owning the lock, it throws IllegalMonitorStateExceptionThen we're checking the application state using isEmpty. If this check fails, we invoke Condition's await method - notEmpty.await. This invocation suspends the running thread and releases the lock. The thread transitions into the WAITING state. In other words, it waits until another thread signals that it can wake up.

Next, we'll examine the signalAll method to awake the waiting threads:

public void signalOnCondition() {
    listLock.lock();
    try {
        // Do work.
    
        System.out.println("I will signal all.");
        notEmpty.signalAll();
    } finally {
        listLock.unlock();
    }
}

In this example, after acquiring the lock, we're invoking the signalAll method on notEmpty. If there are any threads waiting on the notEmpty condition, they'll all wake up. Then they'll contend for acquiring the lock - listLock - to resume their operation.

3. Sample Application

Before going further, we'll create a sample application using what we've learned so far.

Our application is a thread-safe counter that supports increment and decrement operations. Moreover, it has two important properties:

  • We can't decrement if the count is zero.
  • We can't increment if the count is at the upper limit.
public class Counter {

    private final Lock lock = new ReentrantLock();
    private final Condition notZero = lock.newCondition();
    private final Condition notAtLimit = lock.newCondition();
    
    private final int limit = 50;
    private int count = 0;

    public int increment() throws InterruptedException {
        lock.lock();
        try {
            while (count == limit) {
                notAtLimit.await();
            }

            count++;
            notZero.signalAll();

            return count;
        } finally {
            lock.unlock();
        }
    }

    public int decrement() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notZero.await();
            }

            count--;
            notAtLimit.signalAll();
            return count;
        } finally {
            lock.unlock();
        }
    }
}

Here, we're storing the current count in the count variable. We also have two methods: increment and decrement. Since increment and decrement are compound actions, we must provide synchronization. So we're creating a ReentrantLock instance. Also, to satisfy the two properties, we're creating two Condition instances - notZero and notAtLimit.

In the increment method, if the count is at the limit, we're waiting on the notAtLimit condition. At this stage, any thread that attempts to increment will enter the WAITING state and release the lock. In order to resume their execution, they need another thread signaling on the notAtLimit condition. In the decrement method, we're indeed calling notAtLimit.signalAll after decreasing the count.

Similarly, the threads decrementing the counter can also enter the WAITING state. If the count is zero during a decrement, we're calling notZero.await to wait until the count gets bigger than zero. And the increment method signals this after increasing the count.

4. await Modes

So far, we've used the await method that is responsive to interrupts. Next, we'll examine other await variants.

awaitUninterruptibly can't be interrupted. It makes the current thread wait until another thread signals it:

public void awaitUninterruptiblyOnCondition() {
    listLock.lock();
    try {
        while (isEmpty()) {
            System.out.println("I will wait ignoring interrupts");
            notEmpty.awaitUninterruptibly();
        }
    
        // Do work.
    } finally {
        listLock.unlock();
    }
}

Since awaitUninterruptibly doesn't check the thread interruption status, it makes things difficult in terms of task cancellation. For example, ThreadPoolExecutor uses Thread.interrupt as the cancellation mechanism, so it can't stop tasks waiting on awaitUninterruptibly.

Another waiting method is timed await. The current thread waits until it's signaled, interrupted or the specified time elapses:

public void timedAwaitOnCondition() throws InterruptedException {
    listLock.lock();
    try {
        while (isEmpty()) {
            System.out.println("I can be back in one second");
            notEmpty.await(1, TimeUnit.SECONDS);
        }
        // Do work.
    } finally {
        listLock.unlock();
    }
}

Here, if the thread invoking await don't get signaled or interrupted, it'll wake up after one second. Then if it can acquire the lock again, it'll continue its work.

5. signal vs signalAll

Lastly, we'll look at the differences between signal and signalAll.

The signal method selects one thread from the waiting threads and then awakes it. For example, if we have ten threads waiting on a condition, they'll all be in the WAITING state. After the signal invocation, nine threads will remain in the WAITING state.

signalAll, on the other hand, awakes all waiting threads. So after a signalAll invocation, it is possible that all threads are running.

To better understand the difference, we'll use the previous Counter class and add another method:

public int incrementBy(int amount) throws InterruptedException {
    lock.lock();
    try {
        while (count == limit) {
            notAtLimit.await();
        }
 
        count = count + amount;
        notZero.signalAll();
 
        return count;
    } finally {
        lock.unlock();
    }
}

Here, we're adding the incrementBy method that declares the amount parameter. Like the other Counter methods, this one also uses the signalAll method instead of signal.

Next, we'll run some tasks using Counter:

public void allCompletesAfterSignalAll() throws InterruptedException {
    final ExecutorService executorService = Executors.newFixedThreadPool(20);
    final Counter counter = new Counter();

    for (int i = 0; i < 10; i++) {
        executorService.submit(() -> counter.decrement());
    }

    executorService.submit(() -> counter.increment(20));

    executorService.shutdownNow();
    executorService.awaitTermination(1, TimeUnit.SECONDS);

    System.out.println("Count: " + counter.getCount());
}

In this run, we're submitting ten decrement tasks and all will wait since the count is zero. Then the increment task will run making the count 20. It'll also signal all waiting tasks. As a result, ten tasks will awake, and all will decrement the count. The final value of the counter is 10.

If we've used signal instead of signalAll in the incrementBy method, only one task would decrement. Thus the final value would be 19.

In the next run, we'll just change the increment amount from 20 to 1:

public void oneCompletesAfterSignalAll() throws InterruptedException {
    final ExecutorService executorService = Executors.newFixedThreadPool(20);
    final Counter counter = new Counter();

    for (int i = 0; i < 10; i++) {
        executorService.submit(() -> counter.decrement());
    }

    executorService.submit(() -> counter.increment(1));

    // Other code...
}

Here, signalAll awakes all ten threads and they try to acquire the lock. The first one decrements the count to zero and the other nine threads go back to the WAITING state. So it is obvious that the task structure is also important in the final outcome.

6. Summary

In this tutorial, we've investigated how we can use the Condition class in Java. Firstly we examined the basic usage of Condition. Then we've built an application to enhance our understanding. Then we provided details about some of its methods.

Lastly, check out the source code for all examples in this tutorial over on Github.