Recently, on StackOverFlow.com, a question was asked about why java.util.concurrent.ArrayBlockingQueue use while loops instead of if around calls to await(). The accepted answer was that it was due to spurious wakeups. I strongly disagree with this answer and instead believe that it is due to bad programming around race conditions. This is an all too common problem with producer/consumer models from threaded/reentrant applications.
In producer/comsumer applications you have threads which are producing some items and threads which are consuming the items and working on them. This can be a website with servlet threads queueing up aynchronous database requests, a thread reading lines from a file and queueing up work that needs to be done on each line, etc..
Typically you have some sort of blocking queue so that when a producer has a item, it adds it to the end of the queue, possible blocking if the queue has too many items in it already. The consumer, when it needs a new item, gets from the front of the queue, possibly blocking if the queue is empty. It is the fact that both sides are blocking and need to signal the other side is where the complexity and confusion originate.
In the code listed at the bottom of the page, the consumer locks the lock and tests whether the items list is empty. If it is empty then it waits until a producer signals the hasItems condition. However, when an item is added to the list, and the condition is signaled, the consumer goes to re-lock the lock and may end up behind another consumer waiting for the same lock. That other comsumer will get the lock first and de-queue the item that the first consumer was signaled for. This race to the dequeue is why what we call a race condition and it is the real source of the problem.
Here is a more specific enumeration of the race condition:
The race is between getter A and C on who is able to get the lock first to dequeue the new item. The race condition would be solved by changing lines 26 and 51 from if statements to while statements. Once getter A gets the lock it would check again to see if items is empty and would go back to waiting if C had already dequeued. The producer would also check again to see if the list is at capacity.
This is the real problem with many producer/comsumer models instead of spurious wakeups which I'm sure do happen but not nearly as frequently. As an interesting historical note, I had submit corrections to the original O'Reilly Pthreads book which suffered from the same problems -- errata had to be issued and a new version published.
Click to download sample code.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
package com.j256;
import java.util.Vector;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* Little class which demonstrates the perils of using if statements instead of while
* in producer/consumer loops.
*/
public class Foo {
// how many maximum items should the list store
private static final int CAPACITY = 10;
private static final int NUM_PUTTERS = 10;
private static final int NUM_GETTERS = 10;
private Vector<String> items = new Vector<String>(CAPACITY);
private ReentrantLock lock = new ReentrantLock();
private Condition hasSpace = lock.newCondition();
private Condition hasItems = lock.newCondition();
// consumer method
public String get() {
lock.lock();
try {
// see if the list is empty, !!DANGER!! should be a while loop
if (items.isEmpty()) {
try {
// wait for the list to have items
hasItems.await();
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
}
String item = null;
try {
// remove the item from the front of the list
item = items.remove(0);
// signal a waiting producer (if any) that the list has space
hasSpace.signal();
} catch (ArrayIndexOutOfBoundsException e) {
System.err.println("Under capacity: " + items.size());
}
return item;
} finally {
lock.unlock();
}
}
// producer method
public void put(String item) {
lock.lock();
try {
// see if the list is full, !!DANGER!! this should be a while loop
if (items.size() >= CAPACITY) {
try {
// wait for the list to have space
hasSpace.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// add an item to the end of the list
items.add(item);
int size = items.size();
if (size > CAPACITY) {
System.err.println("Over capacity: " + size);
}
// signal a waiting consumer (if any) that the list has an item
hasItems.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
new Foo().doMain(args);
}
private void doMain(String[] args) {
for (int i = 0; i < NUM_GETTERS; i++) {
new Thread(new Getter()).start();
}
for (int i = 0; i < NUM_PUTTERS; i++) {
new Thread(new Putter()).start();
}
new Putter();
}
private class Getter implements Runnable {
public void run() {
while (true) {
get();
}
}
}
private class Putter implements Runnable {
public void run() {
while (true) {
put("foo");
}
}
}
}
|
Free Spam Protection ORMLite Java ORM Android ORM Eggnog Recipe