“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

143 阅读11分钟

难度

初级

学习时间

30分钟

适合人群

零基础

开发语言

Java

开发环境

  • JDK v11
  • IntelliJ IDEA v2018.3

友情提示

  • 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
  • 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!

1.温故知新

前面在《“全栈2019”Java多线程第二十三章:活锁(Livelock)详解》一章中介绍了活锁(Livelock)

《“全栈2019”Java多线程第二十四章:等待唤醒机制详解》一章中介绍了等待唤醒机制

现在我们来讲解生产者与消费者线程

2.什么是生产者与消费者线程?

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

如上图所示,生产者与消费者线程指的是生产者线程和消费者线程,生产者线程负责生产数据,消费者线程负责消费数据。

3.生产者消费者例子

下面我们来书写一个生产者消费者的例子。

根据上一小节所说的“生产者与消费者线程指的是生产者线程和消费者线程,生产者线程负责生产数据,消费者线程负责消费数据。”我们先把要生产的数据定义出来,用类描述一下:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

这个数据类有点简陋,没有任何可以储存信息的地方,我们可以在Data类里面定义一个message属性,用于存储信息

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

Data类我们先写到这。

根据“生产者与消费者线程指的是生产者线程和消费者线程,生产者线程负责生产数据,消费者线程负责消费数据。”描述来看,我们需要生产者线程和消费者线程,但是,没有说几个。这里我们先各来一个:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

其中,producerThread1是生产者线程,consumerThread1是消费者线程。现在这两个线程没有执行任何任务,我们需要给它们分配任务。

因为可以有很多线程在生产数据信息,而且多个生产者线程的生产任务是一样的,就像这样:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

所以,我们可以定义一个实现了Runnable接口的生产者任务类,再把实例对象传给多个生产者线程即可

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

接着,我们再定义一个实现了Runnable接口的消费者任务类

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

然后,我们把生产者任务对象和消费者对象创建出来:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

接着,将生产者任务对象和消费者对象传递给生产者线程和消费者线程:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

我请大家再看一遍这个动画:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

生产者线程和消费者线程它们作用的数据对象是同一个,所以可以得知生产者任务和消费者任务共用一个数据对象。

怎么才能让生产者任务和消费者任务共用一个数据对象呢?

我们可以先把数据对象创建出来:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

然后,再把数据对象通过构造方法传递进去:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

接着,我们生产者任务构造方法需要改写:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

然后,我们消费者任务构造方法也需要改写:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

接下来就是如何生产数据的问题了,很简单,我们在生产者任务类中的run()方法里面创建一个需要生产的数据信息:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

接着,就是将数据信息储存在数据对象中,让消费者去消费。于是,我们需要在Data类中写一个存储数据信息的方法:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

写好之后,再来到生产者任务类中,调用数据对象的设置信息方法把已经生产好的数据信息设置到数据对象里面去:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

这样生产者任务基本上写完了,再去书写消费者任务。

消费者任务就是把数据对象里面的数据信息取出来消费,所以数据类中还要有一个获取数据信息的方法:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

接着,我们再去消费者任务类中把数据信息取出来:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

然后,消费该数据信息,这里简单输出一下即可:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

这样,我们的消费者任务类也基本上写完了。

接下来,我们来启动线程:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

整理上述代码之后再运行程序。

演示:

请写一个生产者消费者线程例子。

请观察程序代码及运行结果。

代码:

Data类:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

Producer类:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

Consumer类:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

Main类:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

结果:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

从运行结果来看,好像符合预期。

该程序就没有什么问题吗?

有,我们再多运行几次看看(大家一定要看到最后):

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

最后一次,我们发现数据信息为null

为什么数据信息会为null?

因为生产者线程还没有生产出数据信息,所以数据对象里面的数据信息为空。

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

如上图所示,当数据对象里面的数据信息为空时,消费者线程不应该获取数据对象里面的数据信息!

代码怎么写呢?

如果是消费者线程获得执行权,先判断数据对象里面的数据信息是否存在,若存在,则取出;若不存在,则等待。

按照这个思路修改消费者线程类:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

上面我们暂时写了判断条件,没有写让消费者线程等待的代码,在上一章《“全栈2019”Java多线程第二十四章:等待唤醒机制详解》中我们学习了等待唤醒机制,知道等待与唤醒的操作都跟同步对象有关,于是我们得把这段代码放在同步代码块里面:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

同步对象为data数据对象。

然后,我们在if语句里面让消费者线程在数据信息为空的情况进行等待:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

好了,我们再运行程序,看看执行结果:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

从运行结果来看,经过改良后的程序好像没有什么问题。

事实真的如此吗?我们再多运行几次看看:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

从运行结果来看,程序不动了。这是怎么回事呢?还是结合动画来说明吧:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

程序不动的原因就是消费者线程wait()之后没有被唤醒。唤醒我们知道,调用同步对象的notify()或notifyAll()方法。

在哪调用呢?

我们应该让生产者线程生产完数据信息之后调用同步对象的notify()或notifyAll()方法:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

再次运行修改后的程序,执行结果:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

从运行结果来看,没有再出现消费者线程等待之后没有被唤醒的情况了。

但是这个程序还是有点小问题,我们来把Main类改改,先启动消费者线程:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

再让主线程睡3秒钟:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

最后再启动生产者线程:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

好了,再运行程序,观察执行结果:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

从运行结果来看,程序果真出了问题。

这个问题是这样出现的,我们先启动了消费者线程,让消费者线程先运行,消费者线程就首先获取了数据对象里面的数据信息:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

结果肯定是null,因为生产者线程还没启动呢,就是生产者线程启动了,它也没同步锁,所以得到的数据信息肯定是null。

既然数据信息为null,那么if语句判断条件肯定为true:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

执行if语句体:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

执行到data.wait()时,消费者线程就等待了,注意:此时message的值为null

接下来,该轮到生产者线程执行了:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

生产者线程把数据信息生产完成之后会把data锁上所有正在等待的线程都唤醒,此时消费者线程被唤醒,唤醒之后接着上一次暂停的地方执行:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

执行到最后的输出语句,输出message的值:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

执行结果:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

问题发生的整个过程分析了一遍,如果还有小伙伴没看懂的请在评论区留言,我会为大家解答。

怎么解决这个问题呢?

将if语句换成while语句即可。if语句是判断一次,while语句是判断多次,而且while语句在消费者线程醒后还会继续判断条件是否成立,这个很关键

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

其他类无需修改,运行程序,执行结果:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

从运行结果来看,问题得到解决。

这里再把消费者任务类完善一下,当消费完数据信息后,将数据对象里面的数据信息设置为null:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

修改完这点之后,再说一个生产数据覆盖问题。先把这个问题重现一下,在生产者任务类里面添加一个计数器,初始值为0:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

接着,在原生产数据信息的基础上加上计数器数值形成新的数据信息:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

然后,让生产者线程不停地产生数据信息:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

并且在生产完数据之后,让当前生产者线程睡1秒钟:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

接着,让消费者也不停地消费数据信息:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

因为消费者线程里面本身就有wait,所以消费者线程不用睡1秒钟。

接着,修改我们的Main类,先启动生产者线程,再让主线程睡5秒钟,最后启动消费者线程:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

其他类无需修改,运行程序,执行结果:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

从运行结果来看,程序有问题,计数器怎么就直接从3开始打印了呢?前面的0、1、2呢?

这个就是生产数据覆盖问题。生产者线程刚刚生产了一个数据信息,还没等消费者线程消费呢,就又被生产者线程拿到执行权又生产了一个数据信息,于是后生产的数据信息把前一个生产的数据信息覆盖了。

怎么解决这个问题呢?

只需让生产者线程在生产完数据信息后wait即可,待消费者线程消费完数据信息后再调用同步对象的notifyAll()方法唤醒生产者线程。

按照上述思路来做,修改生产者任务类,让生产者线程在生产完数据信息后(即还有未消费的数据信息)wait:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

为什么使用while循环进行判断,作用和上面写过的一样,不用多说。

接着,修改消费者任务类,待消费者线程消费完数据信息后再调用同步对象的notifyAll()方法唤醒生产者线程:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

其他类无需修改,运行程序,执行结果:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

从运行结果来看,问题得到了解决。

最后,我们把Main类恢复正常,再把以上代码整理之后运行程序看看。

演示:

请写一个生产者消费者线程例子。

请观察程序代码及结果。

代码:

Data类:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

Producer类:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

Consumer类:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

Main类:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

结果:

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

从运行结果来看,程序没有问题。

GitHub

本章程序GitHub地址:https://github.com/gorhaf/Java2019/tree/master/Thread/Producer-Consumer

总结

  • 生产者与消费者线程指的是生产者线程和消费者线程,生产者线程负责生产数据,消费者线程负责消费数据。
  • 生产者线程与消费者线程间通过wait()、notify()和notifyAll()方法来协作。

至此,Java中生产者消费者线程相关内容讲解先告一段落,更多内容请持续关注。

答疑

如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。

上一章

“全栈2019”Java多线程第二十四章:等待唤醒机制详解

下一章

“全栈2019”Java多线程第二十六章:同步方法生产者与消费者线程

学习小组

加入同步学习小组,共同交流与进步。

  • 方式一:关注头条号Gorhaf,私信“Java学习小组”。
  • 方式二:关注公众号Gorhaf,回复“Java学习小组”。

全栈工程师学习计划

关注我们,加入“全栈工程师学习计划”。

“全栈2019”Java多线程第二十五章:生产者与消费者线程详解

版权声明

原创不易,未经允许不得转载!