Producer Consumer has always been favorite question in java multi-threading interviews.
We will create threads using two ways: Using usual synchronization & Locks.
Synchronization:\
Synchronization is standard way of locking shared resources and provide multi-threading. We will create here two inner classes: Producer & Consumer and will use Stack to share the resources.
Producer:
class Producer extends Thread {
@Override
public void run() {
while (true) {
synchronized (itemStack) {
if (itemStack.size() <= MAX) {
COUNTER++;
itemStack.push(String.valueOf(COUNTER));
System.out.println("producing item ->" + COUNTER
+ " & size of stack is now :: "
+ itemStack.size());
itemStack.notifyAll();
}
while (itemStack.size() == MAX) {
try {
System.out.println("Stack is full & Producer is waiting.");
itemStack.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
Consumer:
private class Consumer extends Thread {
@Override
public void run() {
while (true) {
synchronized (itemStack) {
if (itemStack.size() != EMPTY) {
System.out.println("Consuming item ->"
+ itemStack.pop()
+ " & size of stack is now :: "
+ itemStack.size());
itemStack.notifyAll();
}g
while (itemStack.size() == EMPTY) {
System.out.println("Stack is empty & Consumer is waiting.");
COUNTER = 0;
try {
itemStack.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
}
As you can see here, we are trying to acquire the lock using synchronization on shared object Stack.
We will create threads using two ways: Using usual synchronization & Locks.
Synchronization:\
Synchronization is standard way of locking shared resources and provide multi-threading. We will create here two inner classes: Producer & Consumer and will use Stack to share the resources.
Producer:
class Producer extends Thread {
@Override
public void run() {
while (true) {
synchronized (itemStack) {
if (itemStack.size() <= MAX) {
COUNTER++;
itemStack.push(String.valueOf(COUNTER));
System.out.println("producing item ->" + COUNTER
+ " & size of stack is now :: "
+ itemStack.size());
itemStack.notifyAll();
}
while (itemStack.size() == MAX) {
try {
System.out.println("Stack is full & Producer is waiting.");
itemStack.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
Consumer:
private class Consumer extends Thread {
@Override
public void run() {
while (true) {
synchronized (itemStack) {
if (itemStack.size() != EMPTY) {
System.out.println("Consuming item ->"
+ itemStack.pop()
+ " & size of stack is now :: "
+ itemStack.size());
itemStack.notifyAll();
}g
while (itemStack.size() == EMPTY) {
System.out.println("Stack is empty & Consumer is waiting.");
COUNTER = 0;
try {
itemStack.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
}
As you can see here, we are trying to acquire the lock using synchronization on shared object Stack.
NOTE: Make sure you declare itemStack as static variable or else both threads will deadlock each other.
Working code can be found here.
Lock:
Now let's see how we can rewrite same example using ReentrantLock object which is provided in java.util.concurrent package in jdk 5.
Please see my earlier post on Reentrant Lock if you are new to Lock concepts.
To rewrite producer consumer example using Lock, we will need below piece of code.
private static Lock lock = new ReentrantLock();
private static Condition hasSpace = lock.newCondition();
private static Condition hasItems = lock.newCondition();
First line will create explicit lock object for our example and rest of two lines will provide the conditions required to synchronize the Stack.
Condition objects provides await(), signal() & signalAll() methods similar to wait(), notify() and notifyAll() provided by Object class.
Now, Lets see how we can leverage all of this in our example below.
private class Producer extends Thread {
@Override
public void run() {
while (true) {
try {
lock.lock();
while (itemStack.size() >= MAX) {
System.out.println("Stack is full & Producer is waiting.");
try {
// wait for the list to have space
hasSpace.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
COUNTER++;
itemStack.push(String.valueOf(COUNTER));
System.out.println("producing item number -> "
+ itemStack.size());
hasItems.signalAll();
} finally {
lock.unlock();
}
}
}
}
private class Consumer extends Thread {
@Override
public void run() {
while (true) {
try {
lock.lock();
while (itemStack.size() == EMPTY) {
System.out.println("Stack is empty & Consumer is waiting.");
try {
COUNTER = 0;
// wait for the list to have space
hasItems.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
itemStack.pop();
System.out.println("consuming item number -> "+ itemStack.size());
hasSpace.signalAll();
} finally {
lock.unlock();
}
}
}
}
It provides more readability to synchronization code since now you have separate object for locking and so you know what exactly you are synchronizing on. In synchronized version, remember, we were relying on Stack object for locking and sometime this can lead to more confusion and can create deadlocks as well. (Hint: try synchronized version with Stack object as non-static reference.)
However, here since locking is explicit, you need to be alert to unlock the lock object at the end of it can create issues later in the code.
Working code can be found here.
Hope, this will help you understand synchronization and locking concepts.
Working code can be found here.
Lock:
Now let's see how we can rewrite same example using ReentrantLock object which is provided in java.util.concurrent package in jdk 5.
Please see my earlier post on Reentrant Lock if you are new to Lock concepts.
To rewrite producer consumer example using Lock, we will need below piece of code.
private static Lock lock = new ReentrantLock();
private static Condition hasSpace = lock.newCondition();
private static Condition hasItems = lock.newCondition();
First line will create explicit lock object for our example and rest of two lines will provide the conditions required to synchronize the Stack.
Condition objects provides await(), signal() & signalAll() methods similar to wait(), notify() and notifyAll() provided by Object class.
Now, Lets see how we can leverage all of this in our example below.
private class Producer extends Thread {
@Override
public void run() {
while (true) {
try {
lock.lock();
while (itemStack.size() >= MAX) {
System.out.println("Stack is full & Producer is waiting.");
try {
// wait for the list to have space
hasSpace.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
COUNTER++;
itemStack.push(String.valueOf(COUNTER));
System.out.println("producing item number -> "
+ itemStack.size());
hasItems.signalAll();
} finally {
lock.unlock();
}
}
}
}
private class Consumer extends Thread {
@Override
public void run() {
while (true) {
try {
lock.lock();
while (itemStack.size() == EMPTY) {
System.out.println("Stack is empty & Consumer is waiting.");
try {
COUNTER = 0;
// wait for the list to have space
hasItems.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
itemStack.pop();
System.out.println("consuming item number -> "+ itemStack.size());
hasSpace.signalAll();
} finally {
lock.unlock();
}
}
}
}
So As you can see now, we are acquiring the lock explicitly at the beginning of run() method and releasing it in finally block. Also, lock object provides separate conditions for Producer & Consumer objects as well and simplifies the code further because you know here which condition should await and which one should be signaled. (Check synchronized version for wait & notify conditions to compare against).
It provides more readability to synchronization code since now you have separate object for locking and so you know what exactly you are synchronizing on. In synchronized version, remember, we were relying on Stack object for locking and sometime this can lead to more confusion and can create deadlocks as well. (Hint: try synchronized version with Stack object as non-static reference.)
However, here since locking is explicit, you need to be alert to unlock the lock object at the end of it can create issues later in the code.
Working code can be found here.
Hope, this will help you understand synchronization and locking concepts.