先看一个有问题的只能轮替发生的生产者-消费者模型代码(源自http://www.iteye.com/problems/96126的问题):
//生产/消费者模式
public class Basket {
Lock lock = new ReentrantLock();
// 产生Condition对象
Condition produced = lock.newCondition();
Condition consumed = lock.newCondition();
boolean available = false;
public void produce() throws InterruptedException {
lock.lock();
try {
if (available) {
produced.await(); // 放弃lock进入睡眠
}
// 生产
System.out.println("put");
available = true;
consumed.signal(); // 发信号唤醒等待这个Condition的线程
} finally {
lock.unlock();
}
}
public void consume() throws InterruptedException {
lock.lock();
try {
if (!available) {
consumed.await(); // 放弃lock进入睡眠
}
/* 消费 */
System.out.println("get");
available = false;
produced.signal(); // 发信号唤醒等待这个Condition的线程
} finally {
lock.unlock();
}
}
}
测试程序:
public static void main(String[] args) throws InterruptedException {
final Basket basket = new Basket();
// 定义一个producer
Runnable producer = new Runnable() {
public void run() {
try {
basket.produce();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
};
// 定义一个consumer
Runnable consumer = new Runnable() {
public void run() {
try {
basket.consume();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
};
// 各产生10个consumer和producer
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 4; i++)
service.submit(producer);
// Thread.sleep(2000);
for (int i = 0; i < 4; i++)
service.submit(consumer);
service.shutdown();
}
本来想要的效果是一次put之后一次get,但是实际情况可能会出现两次put,或者先出现两次get,即在没有put之前就已经有get了,更甚至还可能出现程序卡死,即出现了4次put,3次get后停住了,或者3次put、4次get后停住了的现象。
原因分析:
当经过put、get之后,假如此时available为true,对于produce()方法可能出现下面情况:
一个线程在等待lock;
一个线程处于await
此时其他线程在调用consume()方法后,会把available设为false,并发送给生产线程发送信号,来唤醒处于await()的线程,之后会调用unlock()方法,让处于等待lock()的线程去竞争这个锁。此时会出现两种情况:
1. 处于等待lock锁的线程竞争到锁
2. 处于await的线程被唤醒,获取锁
如果是第2种情况,则一切正常。但是如果是等待lock()锁的线程竞争到锁,会出现下面情况:
由于处于await的线程后获取锁,但是此时available已经为true了,由于使用if,而不是while自旋锁,因此就会开始说的哪几种情况。
何为Java中的自旋锁:为了防止假唤醒,保存信号的成员变量放在一个while循环里接受检查,而不是在if表达式里。这样的一个while循环叫做自旋锁(校注:这种做法要慎重,目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大)。被唤醒的线程会自旋直到自旋锁(while循环)里的条件变为false。