难度
初级
学习时间
30分钟
适合人群
零基础
开发语言
Java
开发环境
- JDK v11
- IntelliJIDEA v2018.3
友情提示
- 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
- 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!
1.温故知新
前面在《“全栈2019”Java多线程第三十三章:await与signal/signalAll》一章中介绍了Condition对象的await()方法与signal()/signalAll()方法。
在《“全栈2019”Java多线程第三十四章:超时自动唤醒被等待的线程》一章中介绍了Condition对象的await(long time, TimeUnit unit)方法。
在《“全栈2019”Java多线程第三十五章:如何获取线程被等待的时间?》一章中介绍了Condition对象的awaitNanos(long nanosTimeout)方法。
在《“全栈2019”Java多线程第三十六章:如何设置线程的等待截止时间》一章中介绍了Condition对象的awaitUntil(Date deadline)方法。
在《“全栈2019”Java多线程第三十七章:如何让等待的线程无法被中断》一章中介绍了Condition对象的awaitUninterruptibly()方法。
在《“全栈2019”Java多线程第三十八章:从零手写一个线程安全缓冲区》一章中介绍了Lock与Condition实战项目:从零手写一个线程安全缓冲区。
现在我们来介绍用显式锁Lock与Condition对象来实现生产者与消费者模型。
2.什么是生产者消费者模型?
如上图所示,生产者与消费者线程指的是生产者线程和消费者线程,生产者线程负责生产数据,消费者线程负责消费数据。
虽然之前在《“全栈2019”Java多线程第二十五章:生产者与消费者线程详解》一章中介绍了生产者与消费者线程。
也在《“全栈2019”Java多线程第二十六章:同步方法生产者与消费者线程》一章中介绍了用同步方法来实现生产者与消费者线程例子。
但我们还没有用显式锁Lock与Condition对象来实现生产者与消费者模型,本章就来实现。
不清楚生产者消费者模型的小伙伴请一定要先前去阅读上面这两章内容,再来看本章内容。
3.显式锁Lock与Condition实现生产者消费者模型
首先,我们创建生产者:
其次,再创建消费者:
生产者与消费者里面的run()方法不着急完善,先来把生产者与消费者创建出来:
然后,创建出对应的生产者线程与消费者线程:
接着,启动线程:
生产者要生产什么数据和消费者要消费什么数据都还没有,所以这里把数据类创建出来:
之后,我们把数据对象也创建出来:
而且还要把数据对象传递给生产者和消费者它们:
这样做的目的是让生产者把生产的信息放在数据对象中,再让消费者从数据对象中获取要消费的信息。
这样的话,生产者需要一个有参构造方法,并记录数据对象:
消费者也需要一个有参构造方法,也需要记录数据对象:
接着,在数据类中定义一个用于存储信息的变量,变量类型暂时为String类型:
然后,我们生产者的任务是将生产出来的信息存入数据对象中的message变量中,所以数据类需要提供一个设置信息的方法:
同理,我们消费者的任务是取出存在数据对象中的信息,然后进行消费,所以数据类需要提供一个获取信息的方法:
现在我们可以去完善生产者和消费者的run()方法了。
生产者不停地生产数据信息:
消费者不停地消费数据信息:
写了这么多,还没有运行看看,接下来,我们来运行看看。
不过,在运行之前,为了输出效果看得更明显,我们将在设置数据信息的时候带上线程名称+编号:
另外,消费者这边让消费者线程每次消费完数据信息之后睡1秒钟,以免输出太快:
运行程序,执行结果:
从运行结果来看,不符合预期。
第一,我们取到null了,原因是消费者线程先执行获取数据信息操作,此时生产者线程还没来得及生产数据信息。
解决办法:当生产者线程还没生产出数据信息时,就算消费者线程拿到执行权也必须等待,等待生产者线程生产完数据信息之后再唤醒消费者线程。
第二,当消费者线程正在消费数据信息或还没消费数据信息时,生产者线程依然还在不停地在生产数据信息,这是不合理的。
解决办法:当数据信息还未被消费时,生产者线程就算拿到执行权也必须等待,等待消费者线程消费完数据信息之后再唤醒生产者线程。
综上所述,我们需要同步锁和等待唤醒机制。同步锁本章就使用显式锁Lock,等待唤醒机制需要Condition对象来实现。
修改我们的数据类,创建出显式锁:
接着,在创建Condition对象的时候,可以参考我们上一章《“全栈2019”Java多线程第三十八章:从零手写一个线程安全缓冲区》中使用的技巧:创建两个Condition对象,一个用于设置数据信息,一个用于获取数据信息。
如上所述,来修改数据类:
紧接着,给setMessage()方法内部加上锁:
再给getMessage()方法内部加上锁:
还记得上面刚刚说过通过判断数据信息是否被消费来决定让生产者线程和消费者线程进行等待,那么这个判断条件我们就定义一个boolean类型的变量好了,当数据信息被消费时为true,当数据信息被生产时为false。
如上所述,修改数据类,新增一个boolean类型的变量empty:
然后,在setMessage()方法里面判断当数据信息未被消费(即empty为false)时,生产者线程进行等待:
await()方法会抛出异常,此处我们将其做throws处理:
接着,当我们设置完数据信息之后,将empty变量设置为false:
然后,唤醒被等待消费者线程:
同样的,getMessage()方法也需要判断,当数据信息被消费时,使消费者线程等待:
await()方法会抛出异常,我们将其throws处理:
接着,当我们消费完数据信息之后,将empty变量设置为true:
然后,唤醒被等待生产者线程:
接下来,我们该去修改Main类中的代码了。在此之前,将写在消费者类中的使当前线程睡1秒钟代码移至数据类中的setMessage()方法中来。
需要移动的代码:
移至数据类中的setMessage()方法里来:
因为getMessage()方法和setMessage()方法都抛出异常,所以调用它们的地方需要处理抛出的异常。
于是乎,生产者类需要修改:
同样的,消费者类也需要修改:
Main类无需修改。
将上述代码整理之后再运行。
演示:
请使用显式锁Lock与Condition对象完成生产者消费者模型。
请观察程序代码及结果。
代码:
Data类:
Producer类:
Consumer类:
Main类:
结果:
从运行结果来看,符合预期。
至此,我们用显式锁Lock和Condition对象实现了生产者消费者模型。
最后,希望大家可以把这个例子照着写一遍,然后再自己默写一遍,方便以后碰到类似的面试题可以轻松应对。
祝大家编码愉快!
GitHub
本章程序GitHub地址:https://github.com/gorhaf/Java2019/tree/master/Thread/Producer-Consumer
总结
- await()方法使当前线程在等待,直到它被唤醒,通常由被唤醒或中断。
- signal()方法唤醒正在此对象监视器上等待的单个线程,选择是随机的。
- signalAll()方法作用是唤醒正在此对象监视器上等待的所有线程。
至此,Java中用显式锁Lock和Condition对象实现生产者消费者模型相关内容讲解先告一段落,更多内容请持续关注。
答疑
如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。
上一章
“全栈2019”Java多线程第三十八章:从零手写一个线程安全缓冲区
下一章
“全栈2019”Java多线程第四十章:ReadWriteLock读写锁
学习小组
加入同步学习小组,共同交流与进步。
- 方式一:关注头条号Gorhaf,私信“Java学习小组”。
- 方式二:关注公众号Gorhaf,回复“Java学习小组”。
全栈工程师学习计划
关注我们,加入“全栈工程师学习计划”。
版权声明
原创不易,未经允许不得转载!