A classic problem in concurrent programming is the producer-consumer problem. We have a data buffer, one or more producers of data that save it in the buffer and one or more consumers of data that take it from the buffer.
As the buffer is a shared data structure, we have to control the access to it using a synchronization mechanism such as the synchronized keyword, but we have more limitations. A producer can't save data in the buffer if it's full and the consumer can't take data from the buffer if it's empty.
For these types of situations, Java provides the wait(), notify(), and notifyAll() methods implemented in the Object class. A thread can call the wait() method inside a synchronized block of code. If it calls the wait() method outside a synchronized block of code, the JVM throws an IllegalMonitorStateException exception. When the thread calls the wait() method, the JVM puts the thread to sleep and releases the object that controls the synchronized block of code that it's executing and allows the other threads to execute other blocks of synchronized code protected by that object. To wake up the thread, you must call the notify() or notifyAll() method inside a block of code protected by the same object.
In this recipe, you will learn how to implement the producer-consumer problem using the synchronized keyword and the wait(), notify(), and notifyAll() methods.
Getting ready
The example of this recipe has been implemented using the Eclipse IDE. If you use Eclipse or other IDE such as NetBeans, open it and create a new Java project.
How to do it...
Follow these steps to implement the example:
1. Create a class named EventStorage. It has two attributes: an int attribute called maxSize and a LinkedList<Date> attribute called storage.
2. Implement the constructor of the class that initializes the attributes of the class.
public EventStorage(){
maxSize=10;
storage=new LinkedList<>();
}
3. Implement the synchronized method set() to store an event in the storage.
First, check if the storage is full or not. If it's full, it calls the wait() method until the storage has empty space. At the end of the method, we call the notifyAll() method to wake up all the threads that are sleeping in the wait() method.
public synchronized void set(){
while (storage.size()==maxSize){
System.out.printf("Set: %d",storage.size());
notifyAll();
}
4. Implement the synchronized method get() to get an event for the storage.
First, check if the storage has events or not. If it has no events, it calls the wait() method until the storage has some events. At the end of the method, we call the notifyAll() method to wake up all the threads that are sleeping in the wait() method.
public synchronized void get(){
while (storage.size()==0){
System.out.printf("Get: %d: %s",storage.
size(),((LinkedList<?>)storage).poll());
notifyAll();
}
5. Create a class named Producer and specify that it implements the Runnable interface. It will implement the producer of the example.
public class Producer implements Runnable {
6. Declare an EventStore object and implement the constructor of the class that initializes that object.
private EventStorage storage;
public Producer(EventStorage storage){
this.storage=storage;
}
7. Implement the run() method that calls 100 times the set() method of the EventStorage object.
@Override
public void run() {
for (int i=0; i<100; i++){
storage.set();
} }
8. Create a class named Consumer and specify that it implements the Runnable interface. It will implement the consumer for the example.
public class Consumer implements Runnable {
9. Declare an EventStorage object and implement the constructor of the class that initializes that object.
private EventStorage storage;
public Consumer(EventStorage storage){
this.storage=storage;
}
10. Implement the run() method. It calls 100 times the get() method of the EventStorage object.
@Override
public void run() {
for (int i=0; i<100; i++){
storage.get();
} }
11. Create the main class of the example by implementing a class named Main and add to it the main() method.
public class Main {
12. Create an EventStorage object.
EventStorage storage=new EventStorage();
13. Create a Producer object and Thread to run it.
Producer producer=new Producer(storage);
Thread thread1=new Thread(producer);
14. Create a Consumer object and Thread to run it.
Consumer consumer=new Consumer(storage);
Thread thread2=new Thread(consumer);
15. Start both threads.
thread2.start();
thread1.start();
How it works...
The key to this example is the set() and get() methods of the EventStorage class.
First of all, the set() method checks if there is free space in the storage attribute. If it's full, it calls the wait() method to wait for free space. When the other thread calls the notifyAll() method, the thread wakes up and checks the condition again. The notifyAll() method doesn't guarantee that the thread will wake up. This process is repeated until there is free space in the storage and it can generate a new event and store it.
The behavior of the get() method is similar. First, it checks if there are events on the storage. If the EventStorage class is empty, it calls the wait() method to wait for events.
Where the other thread calls the notifyAll() method, the thread wakes up and checks the condition again until there are some events in the storage.
You have to keep checking the conditions and calling the wait() method in a while loop. You can't continue until the condition is true.
If you run this example, you will see how producer and consumer are setting and getting the events, but the storage never has more than 10 events.
There's more...
There are other important uses of the synchronized keyword. See the See also section for other recipes that explain the use of this keyword.
See also
f The Arranging independent attributes in synchronized classes recipe in Chapter 2, Basic Thread Synchronization