Thread in Java : Part 4
In Part 3 of threads we looked at synchronization, monitor model of synchronization. Now, today we will look at Consumer-Producer Thread concept.
Putting It Together:
- The code in this is an example of thread interaction that demonstrates the use of wait and notify methods to solve a classic producer-consumer problem.
- Start by looking at the outline of the stack object and the details of the threads that access it.
- Then look at the details of the stack and the mechanisms used to protect the stack’s data and to implement the thread communication based on the stack’s state.
- The example stack class, called SyncStack to distinguish it from the core clas java.util.Stack, offers the following public API:
public synchronized void push(char c);
public synchronized char pop();
The Producer Thread:
The producer thread generates new characters to be placed on the stack. the code below shows the Producer class.
- package mod13;
- public class Producer implements Runnable
- {
- private SyncStack theStack;
- private int num;
- private static int counter = 1;
- public Producer(SyncStack s)
- {
- theStack = s;
- num = counter++;
- }
- public void run()
- {
- char c;
- for(int i=0;i<200;i++)
- {
- c = (char) (Math.random()*26+’A’);
- theStack.push(c);
- System.out.println(“Producer” + num + “: ” + c);
- try{
- Thread.sleep((int) (Math.random() * 300));
- }catch(InterruptedException e)
- {
- // ignore it
- }
- }
- }// end run method
- }// end Producer class
- This example generates 200 random peerages characters and pushes them onto the stack with a random delay of 0-300 milliseconds between each push.
- Each pushed character is reported on the console, along with an identifier for which producer thread is executing.
The Consumer Thread:
The Consumer thread removes characters from the stack. the given code below shows the Consumer class.
- package mod13;
- public class Consumer implements Runnable
- {
- private SyncStack theStack;
- private int num;
- private static int counter = 1;
- public Consumer(SyncStack s)
- {
- theStack = s;
- num = counter++;
- }
- public void run()
- {
- char c;
- for(int i = 0; i<200 ; i++)
- {
- c = theStack.pop();
- System.out.println(“Consumer” + num + “: ” + c);
- try{
- Thread.sleep((int) (Math.random() * 300));
- }catch(InterruptedException e)
- {
- // ignore it
- }
- }
- }// end run method
- }// end Consumer class
- This example collects 200 characters fro the stack, with a random delay of 0-300 milliseconds between each attempt.
- Each uppercase character is reported on the console, along with an identifier to identify the consumer thread that is executing.
- Now consider construction of the stack class. You are going to create a stack that has a seemingly limitless size, using the ArrayList class.
- With this design, your threads have only to communicate based on whether the stack is empty.
The SyncStack Class:
A newely constructed SyncStack object’s buffer should be empty. You can use the following code to build your class:
- public class SyncStack
- {
- public List<Character> buffer = new ArrayList<Character>(400);
- public synchronized char pop()
- {
- // pop code here
- }
- public synchronized void push(char c)
- {
- // push code here
- }
- }
There are no constructors. It is considered good style to include a constructor, but it has been omitted for brevity.
The pop Method:
- Now consider the push and pop methods. They must be synchronized to protect the shared buffer.
- In addition, if the stack is empty in the pop method, the executing thread must wait. When the stack in the push method is no longer empty, waiting threads are notified.
- The code given below shows the pop method:
- public synchronized char pop()
- {
- char c;
- while(buffer.size() == 0)
- {
- try{
- this.wait();
- }catch(InterruptedException e)
- {
- // ignore it…
- }
- }
- c = buffer.remove(buffer.size()-1);
- return c;
- }
- The wait call is made with respect to the stack object that shows how the rendezvous is being made with a particular object.
- Nothing can be popped from the stach when it is empty, so a thread trying to pop data from the stack must wait until the stack is no longer empty.
- The wait call is placed in a try-catch block because an inturrept call can terminate the thread’s waiting period. The wait must also be within a loop for this example.
- If for some reason (such as an inturrept) the thread wakes up and discovers that the stack is still empty, then the thread must re-enter the waiting condition.
- The pop method for the stack is synchronized for two reasons. First, popping a character off of the stack affects the shared data buffer.
- Second, the call to this.wait() must be within a block that is synchronized on the stack object, which is represented by this.
- The push method uses this.nootify() to release a thread from the stack object’s wait pool. After a thread is released, it can obtain the lock on the stack and continue executing the pop method, which removes a character from the stack’s buffer.
Note: In pop, the wait method is called before any character is removed from the stack. this is because the removal cannot proceed until some character is available.
- You should also consider error checking. You may notice that there is no explicit code to prevent a stack underflow.
- This is not necessary because the onkly way to remove characters from the stack is through the pop method, and this method causes the executing thread to enter the wait state if no character is available. Therefore, error checking is unnecessary.
The push Method:
- The push method is similar to pop method. It affects the shared buffer amd must also be synchronized.
- In addition, because the push method adds a character to the buffer, it is responsible for notifying threads that are waiting for a non-empty stack.
- This notification is done with respect to the stack object.
- The code below shows the push method:
- public synchronized void push(char c)
- {
- this.notify();
- buffer.add(c);
- }
- The call to this.notify() serves to release a single thread that called wait because the stack is empty.
- Calling notify before the shared data is changed is of no consequence. The stack object’s lock is released only upon exit from the synchronized block, so thread waiting for that lock can obtain it while the stack data are being changed by the pop method.
Putting all of pieces together , the code given below shows the complete SyncStack class:
- package mod13;
- import java.util.*;
- public class SyncStack
- {
- private List<Character> buffer = new ArrayList<Character>(400);
- public synchronized char pop()
- {
- char c;
- while(buffer.sized() == 0)
- {
- try{
- this.wait();
- }catch(InterruptedException e)
- {
- // ignore it
- }
- }
- c = buffer.remove(buffer.size() – 1);
- return c;
- }
- public synchronized void push(char c)
- {
- this.notify();
- buffer.add(c);
- }
- }
The SyncTest Example:
- You must assemble the producer, consumer, and stack code into completer classes. A test harness is required to bring these pieces together.
- Pay particular attention to how SyncTest creates only one stack object that is shared by all threads.
- The code given below shows the SyncTest class:
- package mod13;
- public class SyncTest
- {
- public static void main(String args[])
- {
- SyncStack stack = new SyncStack();
- Producer p1 = new Producer(stack);
- Thread prodT1 = new Thread(p1);
- prodT1.start();
- Producer p2 = new Producer(stack);
- Thread prodT2 = new Thread(p2);
- prodT2.start();
- Consumer c1 = new Consumer(stack);
- Thread consT1 = new Thread(c1);
- consT1.start();
- Consumer c2 = new Consumer(stack);
- Thread consT2 = new Thread(c2);
- consT2.start();
- }
- }
The following is an example of the output from java mod13.SyncTest. every this thread code is run, the results vary.
Producer2: F
Consumer1: F
Producer2: K
Consumer2: K
Producer2: T
Producer1: N
Producer1: V
Consumer2: V
Consumer1: N
Producer2: V
Producer2: U
Consumer2: U
Consumer2: V
Producer1: F
Consumer1: F
Producer2: M
Consumer2: M
Consumer2: T