Monday, 20 July 2015

Java Lock and Condition Example using Producer Consumer Solution

You can also solve producer consumer problem by using new lock interface and condition variable instead of using synchronized keyword and wait and notify methods.  Lock provides an alternate way to achieve mutual exclusion and synchronization in Java. Advantage of Lock over synchronized keyword is well known, explicit locking is much more granular and powerful than synchronized keyword, for example, scope of lock can range from one method to another but scope of 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, second thread can call condition.signal() or condition.signalAll() method to notify that its time to wake-up, condition has been changed. Though Lock and Condition variables are powerful they are slightly difficult to use for first timers. If you are used to locking using synchronized keyword, you will using Lock painful because now it becomes developer's responsibility to acquire and release lock. Anyway, you can follow 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 classic Producer Consumer problem. In order to deeply understand these new concurrency concepts, I also suggest to take a look at Java 7 Concurrency Cookbook, Its one of the best book in Java concurrency with some good non trivial examples.


How to use Lock and Condition variable in Java

You need to be little bit careful when you are using Lock class in Java. Unlike synchronized keyword, 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 has suggested 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 lock is released even if try block throws any exception.


How to create Lock and Condition in Java?

Since Lock is an interface, you cannot create object of Lock class, but don't worry, Java provides two implementation of Lock interface, ReentrantLock and ReentrantReadWriteLock. You can create object of any of this class 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. One you have an object, you can call lock() method to acquire lock and unlock() method to release lock. Make sure you follow the idiom shown above to release 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 condition variable by calling lock.newCondtion() method. This class provides method to wait on a condition and notify waiting threads, much like 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 thread on a condition, instead of calling notify() and notifyAll(), you should use signal() and signalAll() methods. Its better practice to use signalAll() to notify all threads which are waiting on some condition, similar to using notifyAll() instead of notify().

Just like multiple wait method, you also have three version of await() method, first await() which causes current thread to wait until signalled or interrupted,  awaitNanos(long timeout) which wait until notification or timeout and awaitUnInterruptibly() which causes current thread to wait until signalled. You can not interrupt the thread if its waiting using this method. Here is sample code idiom to use Lock interface in Java :

How to use Lock and Condition variables in Java


Producer Consumer Solution using Lock and Condition

Here is our Java solution to classic Producer and Consumer problem, this time we have used Lock and Condition variable to solve this. If you remember in past, I have shared tutorial to solve producer consumer problem using wait() and notify() and by using new concurrent queue class BlockingQueue. In terms of difficultly, 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 take advantage of 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 object of ProducerConsumerImpl and producer and consumer threads, which are other two classes extends 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 CONSUMER thread has started before PRODUCER thread and found that buffer is empty, so its waiting on condition "bufferNotFull". Later when PRODUCER thread started, it added an element into 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 call to signalAll() our CONSUMER thread wake up and checks the condition again, found that shred queue indeed no more empty now, so it has gone ahead and consumed that element from queue.

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

That's all about how to solve producer consumer problem using lock and condition variable 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 question 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 following tutorials as well.

Java Concurrency Tutorials for Beginners

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

Good Books to Learn and Master Concurrency in Java
  • Java Concurrency in Practice by Brian Goetz (book)
  • Java 7 Concurrency Cookbook of Packet publication
  • The Well-Grounded Java Developer: Vital techniques of Java 7 and polyglot programming (book)

No comments:

Post a Comment