Loading

JUC学习之——生产者消费者问题

前言


前面写过一篇关于生产者消费者问题的博客,但是通过对JUC的学习,发现前面写的存在不少问题,比如使用synchronized锁,以及没有做好防止线程虚假唤醒的措施,故在此重新完善。

synchronized与ReentranLock的比较


前面对ReentranLock的加锁解锁原理的源代码进行了一些分析,synchronized和ReentranLock的比较分析也大致说了一下。
大概的对比如下:

  • 相同点:两者都是协调多线程对共享资源的访问,保证线程的安全,两者加的锁都是独占式的可重入悲观锁。
  • 不同点:
    1. synchronized是java语言的关键字,其实现完全依靠JVM;ReentranLock是自JDK1.5以来引进的一个API,需要通过try/catch以及lock()/unlock()来手动控制获取锁和释放锁。
    2. synchronized不可响应中断且只能创建非公平锁;ReentranLock可以响应中断且可以创建公平锁
    3. 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();
    }
}

执行结果如下:
在这里插入图片描述
可以发现程序执行的数据正常,至此,对于生产者消费者的进一步分析暂告一段落。

 

posted @ 2020-06-25 22:48  Icdd  阅读(79)  评论(0)    收藏  举报