JUC学习之——生产者消费者问题
前言
前面写过一篇关于生产者消费者问题的博客,但是通过对JUC的学习,发现前面写的存在不少问题,比如使用synchronized锁,以及没有做好防止线程虚假唤醒的措施,故在此重新完善。
synchronized与ReentranLock的比较
前面对ReentranLock的加锁解锁原理的源代码进行了一些分析,synchronized和ReentranLock的比较分析也大致说了一下。
大概的对比如下:
- 相同点:两者都是协调多线程对共享资源的访问,保证线程的安全,两者加的锁都是独占式的可重入悲观锁。
- 不同点:
- synchronized是java语言的关键字,其实现完全依靠JVM;ReentranLock是自JDK1.5以来引进的一个API,需要通过try/catch以及lock()/unlock()来手动控制获取锁和释放锁。
- synchronized不可响应中断且只能创建非公平锁;ReentranLock可以响应中断且可以创建公平锁
- synchronized如果需要绑定多个条件就必须再嵌套锁,而ReentranLock绑定多个锁只需要实例化Condition类,同时绑定多个Condition类即可。
使用ReentranLock解决生产者消费者问题
资源类:Store类,用于生产和消费,是生产者和消费者的共享资源。对这个类的操作需要调用lock()/unlock()方法进行加锁和解锁。
假设有生产者A和消费者D分别进行生产和消费,当商品数量<10时,生产者A可以生产,每当生产一件商品时,尝试唤醒消费者D进行消费,当商品数量达到10时,生产者停止生产;当商品数量>0时,消费者D可以消费,每当消费一件商品时,尝试唤醒生产者线程进行生产,当商品数量为0时,消费者停止消费。
我们假设上述过程进行20轮,即每个生产者生产20次商品,每个消费者消费20次商品。
上述过程用代码描述如下:
class Store{
//标记商品数量
private Integer count = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//生产商品的方法
public void increment() {
//手动获取锁
lock.lock();
try {
//商品数量到达10件时,阻塞当前生产者线程
if(count == 10){
condition.await();
}
//每生产1件商品,就可以通知消费者来消费,即唤醒所有线程
System.out.println(Thread.currentThread().getName() + "\t生产了第" + (++count) + "件商品");
condition.signalAll();
} catch(Exception e) {
e.printStackTrace();
} finally {
//手动释放锁
lock.unlock();
}
}
//消费商品的方法
public void decrement() {
//手动获取锁
lock.lock();
try {
//当商品数量为0时,阻塞当前消费者线程
if(count == 0){
condition.await();
}
//每消费一件商品,就可以通知生产者来生产,即唤醒所有线程
System.out.println(Thread.currentThread().getName() + "\t消费了第" + (count--) + "件商品");
condition.signalAll();
} catch(Exception e) {
e.printStackTrace();
} finally {
//手动释放锁
lock.unlock();
}
}
}
public class ProduceConsumerDemo1 {
public static void main(String[] args) {
Store store = new Store();
//创建生产者
createProducer(store, "A");
//创建消费者
createConsumer(store, "D");
}
//创建生产者并创建和启动线程
private static void createProducer(Store store, String producer) {
new Thread(() -> {
for (int i = 0; i < 10; i++) {
store.increment();
}
}, producer).start();
}
//创建消费者并创建和启动线程
private static void createConsumer(Store store, String consumer) {
new Thread(() -> {
for (int i = 0; i < 10; i++) {
store.decrement();
}
}, consumer).start();
}
}
上述程序执行结果如下图:

可以发现运行正常,生产者线程A和消费者线程D先后访问资源。按照程序设计的一样,初始商品数量为0,则消费者线程D被阻塞,生产者线程A进行生产,当商品数量到达10个时,生产者线程A自行阻塞,释放锁。此时被唤醒的消费者线程D获得锁进行消费。
这样看来似乎已经解决了生产者消费者问题,但是这段程序中存在着一个漏洞
//当商品数量为0时,阻塞当前消费者线程
if(count == 0){
condition.await();
}
//商品数量到达10件时,阻塞当前生产者线程
if(count == 10){
condition.await();
}
这两段代码存在着很严重的问题,也许当前一个生产者一个消费者,我们看不出来有什么问题,但是如果存在两个生产者线程A、B以及两个消费者线程C、D,我们就能发现有以下错误:

发现出现了负数,出现这样的原因是某个生产者线程生产完一件商品后,执行了signalAll()方法,这样就将其他所有阻塞状态的线程唤醒,包括两个消费者线程,在其中一个消费者线程执行正常消费过程后,此时的商品余量为0,然而另一个消费者线程抢占锁,继续进行消费,这样便使得出现了负数,导致这样的结果的原因是
if(count == 0){
condition.await();
}
最初,初始状态下(即生产者尚未生产商品,商品数量为0)两个消费者线程经过if()的判断,都被阻塞,当生产者生产完毕时,唤醒两个消费者线程,这两个消费者线程便不需要再次判断当前是否还有商品,当一个消费者线程进行了正常消费,另一个消费者线程直接跳出if分支,抢占锁进行消费,便出现了这种错误。反过来,两个生产者线程也会出现上述类似的错误:

商品数量到达10后,其中一个生产者线程释放锁,另一个生产者线程直接跳出if语句,抢占锁进行继续生产。
上述程序出现的这些错误我们将其统称为**线程的虚假唤醒**。
面对这个线程虚假唤醒,解决措施是,将if改成while,在第二个消费者进行消费前,或者第二个生产者进行生产前,再次判断当前的商品数量是否达到阈值,若达到了则继续阻塞该线程。这样便可以有效解决因虚假唤醒造成的数据异常。
修改后的代码如下:
class Store{
private Integer count = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() {
lock.lock();
try {
//防止线程的虚假唤醒
while(count == 10){
condition.await();
}
System.out.println(Thread.currentThread().getName() + "\t生产了第" + (++count) + "件商品");
condition.signalAll();
} catch(Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
//防止线程的虚假唤醒
while(count == 0){
condition.await();
}
System.out.println(Thread.currentThread().getName() + "\t消费了第" + (count--) + "件商品");
condition.signalAll();
} catch(Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ProduceConsumerDemo1 {
public static void main(String[] args) {
Store store = new Store();
//创建生产者
createProducer(store, "A");
createProducer(store, "B");
//创建消费者
createConsumer(store, "D");
createConsumer(store, "E");
}
private static void createConsumer(Store store, String consumer) {
new Thread(() -> {
for (int i = 0; i < 20; i++) {
store.decrement();
}
}, consumer).start();
}
private static void createProducer(Store store, String producer) {
new Thread(() -> {
for (int i = 0; i < 20; i++) {
store.increment();
}
}, producer).start();
}
}
执行结果如下:

可以发现程序执行的数据正常,至此,对于生产者消费者的进一步分析暂告一段落。

浙公网安备 33010602011771号