Sunday, September 17, 2023

How to use Lock and Condition variable in Java? Producer Consumer Problem Example Tutorial

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;

/**
 * Java Program to demonstrate how to use Lock and Condition variable in Java by
 * solving Producer consumer problem. Locks are more flexible way to provide
 * mutual exclusion and synchronization in Java, a powerful alternative of
 * synchronized keyword.
 * 
 * @author Javin Paul
 */
public class ProducerConsumerSolutionUsingLock {

    public static void main(String[] args) {

        // Object on which producer and consumer thread will operate
        ProducerConsumerImpl sharedObject = new ProducerConsumerImpl();

        // creating producer and consumer threads
        Producer p = new Producer(sharedObject);
        Consumer c = new Consumer(sharedObject);

        // starting producer and consumer threads
        p.start();
        c.start();
    }

}

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

  • How to use Future and FutureTask class in Java? (solution)
  • How to use a CyclicBarrier class in Java? (example)
  • How to use Callable and Future class in Java? (example)
  • How to use the CountDownLatch utility in Java? (example)
  • How to use Semaphore class in Java? (code sample)
  • How to use the Lock interface in multi-threaded programming? (code sample)
  • How to use Thread pool Executor in Java? (example)
  • how to do inter-thread communication using wait and notify? (solution)
  • How to use ThreadLocal variables in Java? (example)
  • Top 5 Concurrent Collection classes from Java 5 and Java 6 (read here)
  • 50 Java Thread Questions for Senior and Experienced Programmers (questions)
  • What is the difference between CyclicBarrier and CountDownLatch in Java? (answer)
  • 10 Multi-threading and Concurrency Best Practices for Java Programmers (best practices)

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. 

7 comments:

  1. Thanks for posting a nice explanation of producer consumer problem in java.
    I have a question regarding use of notifyAll() method here.
    As we are using two conditional variables for queue full and empty, can't we just use notify() to notify either one consumer or producer(depending upon the conditional variable) ?
    This will save the overhead of waking up multiple threads, even though only one can proceed.

    ReplyDelete
  2. @scanty, you are correct in terms of more threads getting notification but that's done purposefully to avoid risk of notification being lost. I would suggest to read difference between notify and notifyAll in Java to learn more.

    ReplyDelete
  3. Thanks for the reply.
    Consider this scenario with the buffer being empty and the producer trying to insert an element in buffer.
    To send a notification the producer thread must take a lock, which will only be released when the consumer releases the lock temporarily while waiting for the buffer to be non-empty.
    So, when producer gets the lock and adds the element it sends notification, which is received by the consumer.
    Am I missing something here leading to the notification being lost :)

    If there is only one producer and consumer, how will notifyAll help over notify if there is a chance for the notification to get lost with either of the threads?

    ReplyDelete
  4. are the variables buffernotfull and buffer not empty switched here? why will producer sings, buffer not full after adding element. it should signal buffer not empty right?

    ReplyDelete
  5. ReentrantReadWriteLock is not an implementation of Lock interface.It is a implementation of ReadWriteLock Interface

    ReplyDelete
  6. Thank you. I was searching for this example for hours.

    ReplyDelete
  7. I have a question as to why two condition variables are used? I have coded this using a single condition variable and it works fine. (This is essentially the way synchronized/wait/notifyAll works.) Are two condition variables used for clarity or some other reason?

    Thanks,
    Bob E.

    ReplyDelete