How to use Lock and Condition variable in Java? Producer Consumer Problem Example Tutorial (original) (raw)

You can also solve the producer-consumer problem by using a new lock interface and condition variable instead of using the synchronized keyword and wait and notify methods. The lock provides an alternate way to achieve mutual exclusion and synchronization in Java. The advantage of a Lock over a synchronized keyword is well known, explicit locking is much more granular and powerful than a synchronized keyword, for example, the scope of a lock can range from one method to another but the scope of a synchronized keyword cannot go beyond one method. Condition variables are instance of java.util.concurrent.locks.Condition class, which provides inter-thread communication methods similar to wait, notify and notifyAll e.g. await(), signal(), and signalAll().

So if one thread is waiting on a condition by calling condition.await() then once that condition changes, the second thread can call condition.signal() or condition.signalAll() method to notify that it's time to wake up, the condition has been changed.

Though Lock and Condition variables are powerful they are slightly difficult to understand and use for first-timers. If you are used to locking using a synchronized keyword, you will use Lock painful because now it becomes the developer's responsibility to acquire and release the lock.

Anyway, you can follow the code idiom shown here to use Lock to avoid any concurrency issue.

In this article, you will learn how to use Lock and Condition variables in Java by solving the classic Producer-Consumer problem. In order to deeply understand these new concurrency concepts.

And, if you are serious about mastering Java multi-threading and concurrency then I also suggest you take a look at the Java Multithreading, Concurrency, and Performance Optimization course by Michael Pogrebinsky on Udemy. It's an advanced course to become an expert in Multithreading, concurrency, and Parallel programming in Java with a strong emphasis on high performance

How to use the Lock and Condition variable in Java? Example

You need to be a little bit careful when you are using the Lock class in Java. Unlike synchronized keywords, which acquire and release lock automatically, here you need to call lock() method to acquire the lock and unlock() method to release the lock, failing to do will result in deadlock, livelock, or any other multi-threading issues.

In order to make developer's life easier, Java designers have suggested the following idiom to work with locks :

Lock theLock = ...; theLock.lock(); try { // access the resource protected by this lock } finally { theLock.unlock(); }

Though this idiom looks very easy, Java developer often makes subtle mistakes while coding e.g. they forget to release lock inside finally block. So just remember to release lock in finally to ensure the lock is released even if try block throws an exception.

How to create Lock and Condition in Java?

Since Lock is an interface, you cannot create an object of the Lock class, but don't worry, Java provides two implementations of the Lock interface, ReentrantLock, and ReentrantReadWriteLock. You can create objects of any of these classes to use Lock in Java.

BTW, the way these two locks are used is different because ReentrantReadWriteLock contains two locks, read lock and write lock. In this example, you will learn how to use ReentrantLock class in Java.

Once you have an object, you can call the lock() method to acquire the lock and unlock() method to release the lock. Make sure you follow the idiom shown above to release the lock inside finally clause.

In order to wait on explicit lock, you need to create a condition variable, an instance of java.util.concurrent.locks.Condition class. You can create a condition variable by calling lock.newCondtion() method.

This class provides a method to wait on a condition and notify waiting for threads, much like the Object class' wait and notify method. Here instead of using wait() to wait on a condition, you call await() method.

Similarly in order to notify waiting for a thread on a condition, instead of calling notify() and notifyAll(), you should use signal() and signalAll() methods. It's better to practice using signalAll() to notify all threads which are waiting on some condition, similar to using notifyAll() instead of notify().

Just like the multiple wait method, you also have three versions of await() method, first await() which causes the current thread to wait until signaled or interrupted, awaitNanos(long timeout) which wait until notification or timeout, and awaitUnInterruptibly() which causes the current thread to wait until signaled.

You can not interrupt the thread if it's waiting using this method.

Here is a sample code idiom to use the Lock interface in Java :

How to use Lock and Condition variables in Java

Producer Consumer Problem Solution using Lock and Condition in Java

Here is our Java solution to the classic Producer and Consumer problem, this time we have used the Lock and Condition variable to solve this. If you remember in past, I have shared tutorials to solve producer-consumer problems using wait() and notify() and by using the new concurrent queue class BlockingQueue.

In terms of difficulty, first, one using wait and notify is the most difficult to get it right and BlockingQueue seems to be far easier compared to that. This solution which takes advantage of the Java 5 Lock interface and Condition variable sits right in between these two solutions.

Explanation of Solution
In this program we have four classes, ProducerConsumerSolutionUsingLock is just a wrapper class to test our solution. This class creates an object of ProducerConsumerImpl and producer and consumer threads, which are the other two classes extend to Thread acts as producer and consumer in this solution.

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

/**

}

class ProducerConsumerImpl { // producer consumer problem data private static final int CAPACITY = 10; private final Queue queue = new LinkedList<>(); private final Random theRandom = new Random();

// lock and condition variables
private final Lock aLock = new ReentrantLock();
private final Condition bufferNotFull = aLock.newCondition();
private final Condition bufferNotEmpty = aLock.newCondition();

public void put() throws InterruptedException {
    aLock.lock();
    try {
        while (queue.size() == CAPACITY) {
            System.out.println(Thread.currentThread().getName()
                    + " : Buffer is full, waiting");
            bufferNotEmpty.await();
        }

        int number = theRandom.nextInt();
        boolean isAdded = queue.offer(number);
        if (isAdded) {
            System.out.printf("%s added %d into queue %n", Thread
                    .currentThread().getName(), number);

            // signal consumer thread that, buffer has element now
            System.out.println(Thread.currentThread().getName()
                    + " : Signalling that buffer is no more empty now");
            bufferNotFull.signalAll();
        }
    } finally {
        aLock.unlock();
    }
}

public void get() throws InterruptedException {
    aLock.lock();
    try {
        while (queue.size() == 0) {
            System.out.println(Thread.currentThread().getName()
                    + " : Buffer is empty, waiting");
            bufferNotFull.await();
        }

        Integer value = queue.poll();
        if (value != null) {
            System.out.printf("%s consumed %d from queue %n", Thread
                    .currentThread().getName(), value);

            // signal producer thread that, buffer may be empty now
            System.out.println(Thread.currentThread().getName()
                    + " : Signalling that buffer may be empty now");
            bufferNotEmpty.signalAll();
        }

    } finally {
        aLock.unlock();
    }
}

}

class Producer extends Thread { ProducerConsumerImpl pc;

public Producer(ProducerConsumerImpl sharedObject) {
    super("PRODUCER");
    this.pc = sharedObject;
}

@Override
public void run() {
    try {
        pc.put();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

}

class Consumer extends Thread { ProducerConsumerImpl pc;

public Consumer(ProducerConsumerImpl sharedObject) {
    super("CONSUMER");
    this.pc = sharedObject;
}

@Override
public void run() {
    try {
        pc.get();
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

}

Output CONSUMER : Buffer is empty, waiting PRODUCER added 279133501 into queue PRODUCER : Signalling that buffer is no more empty now CONSUMER consumed 279133501 from queue CONSUMER : Signalling that buffer may be empty now

Here you can see that the CONSUMER thread has started before the PRODUCER thread and found that the buffer is empty, so it's waiting on condition "bufferNotFull".

Later when the PRODUCER thread started, it added an element into a shared queue and signal all threads (here just one CONSUMER thread) waiting on condition bufferNotFull that this condition may not hold now, wake up and do your work.

Following a call to signalAll() our CONSUMER thread wake up and checks the condition again, found that the shred queue is indeed no more empty now, so it has gone ahead and consumed that element from the queue.

Since we are not using any infinite loop in our program, post this action CONSUMER thread came out of the run() method and the thread is finished. PRODUCER thread is already finished, so our program ends here.

That's all about how to solve the producer-consumer problem using lock and condition variables in Java. It's a good example to learn how to use these relatively less utilized but powerful tools. Let me know if you have any questions about lock interface or condition variables, happy to answer. If you like this Java concurrency tutorial and want to learn about other concurrency utilities, You can take a look at the following tutorials as well.

Java Concurrency Tutorials for Beginners

Thanks for reading this article so far. If you like this Java Lock and Condition variable example then please share it with your friends and colleagues. If you have any questions or feedback then please drop a note.

P.S. - If you are new to Java Multithreading and need a free course to learn essential Java thread concepts then I highly suggest you join this** free Java Multithreading course for beginners**. This is a free Udemy course to learn Java Multithreading for beginners.

And, lastly one question for you? What is the best way to solve producer consumer problem in Java? wait and notify method, BlockingQueue or SynchronousQueue? If you know the answer let us know in comments.