Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)

时间:2022-09-21 10:13:57

Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)


本篇博客由浅入深,先从最基础的生产者消费者问题讲起,然后依次深入


普通的生产者消费者问题

首先我们有一个店员类,模拟进货和卖货操作,店里的商品数量最多为10个
注意这里我们给进货和卖货方法都加了synchronized的关键字

Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)

生产者线程,负责生产商品

Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)

消费者线程,负责消费商品

Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)

好,然后我们先看运行结果,然后我们来仔细分析运行结果并提出改正方案

以下是一个简单的测试类,首先new 了一个营业员,然后分别创建了一个消费者和生产者线程

Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)

执行结果如下:

Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)

结果分析:

通过分析以上运行结果我们可以发现一个很明显的结果,要么先是一大堆生产者集中生产,然后消费者集中消费,而且不管是生产者还是消费者,当商品已满或已进销售完毕之后,他们还在尝试生产和消费。

好,那为什么会造成这样的结果呢?原因就在于我们的店员类的进货和销售方法都带了synchronized 关键字,并且在消费者和生产者线程中分别执行了10的循环,这就导致不论是生产者还是消费者一抢到资源,就执行循环,导致出现集中的生产和消费。


采用等待唤醒机制的生产者消费者问题

添加等待唤醒机制的生产者消费者问题与之前不同的是,增加以下红框中的代码,它的意思是当商品已满时生产者等待,唤醒消费者线程消费,当商品销售完了之后,消费者等待,通知生产者生产,为了放大错误发生的几率,我们对将最大商品数量设置为1,并为生产者线程加了0.2秒的延迟

Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)

Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)

那我们来看采用了等待唤醒机制的生产者消费者问题的执行结果

Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)

结果分析:

首先我们看到运行的结果是很规律的,你生产一个,我消费一个,但是我们仔细观察的话我们会发现一个问题,那就是程序迟迟没有自动终止,这是为什么呢?

原因在于:我们的this.wait()和this.notifyAll()的操作没有成对出现,我们仔细观察我们的代码,如下:我们对等待操作和唤醒操作采用的是if,else的判断语句来执行,这就导致为什么程序无法终止的原因。好,可能你还不明白,那我再讲得简单一点,你仔细想一下,我的生产者和消费者线程中分别有10次的循环,去访问这些公共的商品,但是他们执行的顺序要么是生产者等待,消费者唤醒或者消费者等待,生产者唤醒,这样的执行顺序下去,就会导致什么,就会导致,到最后的时候就会有很大地概率,只有一个线程在那边wait等待,但是其他的线程都执行完了,没有人唤醒它了,所以就会出现程序无法终止的情况。

Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)

那如何解决呢?很简单,只要将if,else去掉就行了,代码如下:

Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)


虚假唤醒

好,那么到这里问题就都解决了吗?不,没有解决,试想一下,我们之前的测试和操作,采用的都只有一个生产者和一个消费者,那么如果我们有多个生产者和多个消费者呢?这个时候就会出现一个著名的问题叫虚假唤醒

测试代码如下 我们创建了多个生产者和消费者线程

public class TestProductorAndConsumer {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer producter = new Producer(clerk);
        Consumer consumer  = new Consumer(clerk);
        new Thread(producter,"生产者A").start();
        new Thread(consumer,"消费者A").start();
        new Thread(producter,"生产者B").start();
        new Thread(consumer,"消费者B").start();
    }
}

运行结果如下:

Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)

结果分析:

分析结果你会发现结果已经严重的出错了,并且出现了很多的负数,问题出在哪里呢?
我们回到之前的进货和销售方法,我们已经知道他们是两个synchronized方法

 /** * 进货操作 */
    public synchronized void purchase() throws InterruptedException {
        if (product >= 1) {
            System.out.println("产品已满!");
            this.wait();
        }
        System.out.println(Thread.currentThread().getName() + ":" + ++product);
        this.notifyAll();
    }

    /** * 卖货操作 */
    public synchronized void sale() throws InterruptedException {
        if (product <= 0) {
            System.out.println("缺货!!");
            this.wait();
        }
        System.out.println(Thread.currentThread().getName()+":" + --product);
        this.notifyAll();
    }

由于我们有多个生产者和多个消费者,那么我们先假设在执行卖货方法sale()方法时,有多个消费者A,B都在外面等待,首先是消费者A进行开始消费,但是它发现店内没货了,于是挂起等待,然后下一个消费者B进入消费,同样地也发现没货,因此也wait等待,之后出现了一个生产者,它生产了一个商品,然后执行了notifyAll()方法唤醒了正在等待的消费者线程们,它们一唤醒,就从原来挂起的位置继续执行下去,因此商品数量就被连续减了两次,就出现负数的运行结果。因此我们就称这种现象为虚假唤醒现象

那如何解决虚假唤醒的现象呢,看如下代码:

 /** * 进货操作 */
    public synchronized void purchase() throws InterruptedException {
        while (product >= 1) {//避免虚假唤醒,使用while循环做判断
            System.out.println("产品已满!");
            this.wait();
        }
        System.out.println(Thread.currentThread().getName() + ":" + ++product);
        this.notifyAll();
    }

    /** * 卖货操作 */
    public synchronized void sale() throws InterruptedException {
        while (product <= 0) {//避免虚假操作,使用while循环做判断
            System.out.println("缺货!!");
            this.wait();
        }
        System.out.println(Thread.currentThread().getName()+":" + --product);
        this.notifyAll();
    }

咦,你会发现这和之前的代码也没什么区别呀,注意,仔细看我们这里将if判断改为了while循环判断,这就能解决虚假唤醒的问题,我们再看运行结果,就正常了

Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)


采用锁机制实现生产者消费者问题

Rhyme/Java多线程之生产者消费者问题(等待唤醒机制、虚假唤醒、锁机制)