母鸡下蛋实例:多线程通信生产者和消费者wait/notify和condition/await/signal条件队列

时间:2023-02-06 14:26:45

简介


多线程通信一直是高频面试考点,有些面试官可能要求现场手写生产者/消费者代码来考察多线程的功底,今天我们以实际生活中母鸡下蛋案例用代码剖析下实现过程。母鸡在鸡窝下蛋了,叫练从鸡窝里把鸡蛋拿出来这个过程,母鸡在鸡窝下蛋,是生产者,叫练捡出鸡蛋,叫练是消费者,一进一出就是线程中的生产者和消费者模型了,鸡窝是放鸡蛋容器。现实中还有很多这样的案例,如医院叫号。下面我们画个图表示下。

母鸡下蛋实例:多线程通信生产者和消费者wait/notify和condition/await/signal条件队列

一对一生产和消费:一只母鸡和叫练


wait/notify

package com.duyang.thread.basic.waitLock.demo;

import java.util.ArrayList;
import java.util.List; /**
* @author :jiaolian
* @date :Created in 2020-12-30 16:18
* @description:母鸡下蛋:一对一生产者和消费者
* @modified By:
* 公众号:叫练
*/
public class SingleNotifyWait { //装鸡蛋的容器
private static class EggsList {
private static final List<String> LIST = new ArrayList();
} //生产者:母鸡实体类
private static class HEN {
private String name; public HEN(String name) {
this.name = name;
} //下蛋
public void proEggs() throws InterruptedException {
synchronized (EggsList.class) {
if (EggsList.LIST.size() == 1) {
EggsList.class.wait();
}
//容器添加一个蛋
EggsList.LIST.add("1");
//鸡下蛋需要休息才能继续产蛋
Thread.sleep(1000);
System.out.println(name+":下了一个鸡蛋!");
//通知叫练捡蛋
EggsList.class.notify();
}
}
} //人对象
private static class Person {
private String name; public Person(String name) {
this.name = name;
}
//取蛋
public void getEggs() throws InterruptedException {
synchronized (EggsList.class) {
if (EggsList.LIST.size() == 0) {
EggsList.class.wait();
}
Thread.sleep(500);
EggsList.LIST.remove(0);
System.out.println(name+":从容器中捡出一个鸡蛋");
//通知叫练捡蛋
EggsList.class.notify();
}
}
} public static void main(String[] args) {
//创造一个人和一只鸡
HEN hen = new HEN("小黑");
Person person = new Person("叫练");
//创建线程执行下蛋和捡蛋的过程;
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
hen.proEggs();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//叫练捡鸡蛋的过程!
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
person.getEggs();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}

如上面代码,我们定义EggsList类来装鸡蛋,HEN类表示母鸡,Person类表示人。在主函数中创建母鸡对象“小黑”,人对象“叫练”, 创建两个线程分别执行下蛋和捡蛋的过程。代码中定义鸡窝中最多只能装一个鸡蛋(当然可以定义多个)。详细过程:“小黑”母鸡线程和“叫练”线程线程竞争锁,如果“小黑”母鸡线程先获取锁,发现EggsList鸡蛋的个数大于0,表示有鸡蛋,那就调用wait等待并释放锁给“叫练”线程,如果没有鸡蛋,就调用EggsList.LIST.add("1")表示生产了一个鸡蛋并通知“叫练”来取鸡蛋并释放锁让“叫练”线程获取锁。“叫练”线程调用getEggs()方法获取锁后发现,如果鸡窝中并没有鸡蛋就调用wait等待并释放锁通知“小黑”线程获取锁去下蛋,如果有鸡蛋,说明“小黑”已经下蛋了,就把鸡蛋取走,因为鸡窝没有鸡蛋了,所以最后也要通知调用notify()方法通知“小黑”去下蛋,我们观察程序的执行结果如下图。两个线程是死循环程序会一直执行下去,下蛋和捡蛋的过程中用到的锁的是EggsList类的class,“小黑”和“叫练”竞争的都是统一把锁,所以这个是同步的。这就是母鸡“小黑”和“叫练”沟通的过程。

母鸡下蛋实例:多线程通信生产者和消费者wait/notify和condition/await/signal条件队列

神马???鸡和人能沟通!!

母鸡下蛋实例:多线程通信生产者和消费者wait/notify和condition/await/signal条件队列

Lock条件队列

package com.duyang.thread.basic.waitLock.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; /**
* @author :jiaolian
* @date :Created in 2020-12-30 16:18
* @description:母鸡下蛋:一对一生产者和消费者 条件队列
* @modified By:
* 公众号:叫练
*/
public class SingleCondition { private static Lock lock = new ReentrantLock();
//条件队列
private static Condition condition = lock.newCondition(); //装鸡蛋的容器
private static class EggsList {
private static final List<String> LIST = new ArrayList();
} //生产者:母鸡实体类
private static class HEN {
private String name; public HEN(String name) {
this.name = name;
} //下蛋
public void proEggs() {
try {
lock.lock();
if (EggsList.LIST.size() == 1) {
condition.await();
}
//容器添加一个蛋
EggsList.LIST.add("1");
//鸡下蛋需要休息才能继续产蛋
Thread.sleep(1000);
System.out.println(name+":下了一个鸡蛋!");
//通知叫练捡蛋
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
} //人对象
private static class Person {
private String name; public Person(String name) {
this.name = name;
}
//取蛋
public void getEggs() {
try {
lock.lock();
if (EggsList.LIST.size() == 0) {
condition.await();
}
Thread.sleep(500);
EggsList.LIST.remove(0);
System.out.println(name+":从容器中捡出一个鸡蛋");
//通知叫练捡蛋
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
} public static void main(String[] args) {
//创造一个人和一只鸡
HEN hen = new HEN("小黑");
Person person = new Person("叫练");
//创建线程执行下蛋和捡蛋的过程;
new Thread(()->{
for (int i=0; i<Integer.MAX_VALUE;i++) {
hen.proEggs();
}
}).start();
//叫练捡鸡蛋的过程!
new Thread(()->{
for (int i=0; i<Integer.MAX_VALUE;i++) {
person.getEggs();
}
}).start();
}
}

如上面代码,只是将synchronized换成了Lock,程序运行的结果和上面的一致,wait/notify换成了AQS的条件队列Condition来控制线程之间的通信。Lock需要手动加锁lock.lock(),解锁lock.unlock()的步骤放在finally代码块保证锁始终能被释放。await底层是unsafe.park(false,0)调用C++代码实现。

多对多生产和消费:2只母鸡和叫练/叫练媳妇


wait/notifyAll

package com.duyang.thread.basic.waitLock.demo;

import java.util.ArrayList;
import java.util.List; /**
* @author :jiaolian
* @date :Created in 2020-12-30 16:18
* @description:母鸡下蛋:多对多生产者和消费者
* @modified By:
* 公众号:叫练
*/
public class MultNotifyWait { //装鸡蛋的容器
private static class EggsList {
private static final List<String> LIST = new ArrayList();
} //生产者:母鸡实体类
private static class HEN {
private String name; public HEN(String name) {
this.name = name;
} //下蛋
public void proEggs() throws InterruptedException {
synchronized (EggsList.class) {
while (EggsList.LIST.size() >= 10) {
EggsList.class.wait();
}
//容器添加一个蛋
EggsList.LIST.add("1");
//鸡下蛋需要休息才能继续产蛋
Thread.sleep(1000);
System.out.println(name+":下了一个鸡蛋!共有"+EggsList.LIST.size()+"个蛋");
//通知叫练捡蛋
EggsList.class.notify();
}
}
} //人对象
private static class Person {
private String name; public Person(String name) {
this.name = name;
}
//取蛋
public void getEggs() throws InterruptedException {
synchronized (EggsList.class) {
while (EggsList.LIST.size() == 0) {
EggsList.class.wait();
}
Thread.sleep(500);
EggsList.LIST.remove(0);
System.out.println(name+":从容器中捡出一个鸡蛋!还剩"+EggsList.LIST.size()+"个蛋");
//通知叫练捡蛋
EggsList.class.notify();
}
}
} public static void main(String[] args) {
//创造一个人和一只鸡
HEN hen1 = new HEN("小黑");
HEN hen2 = new HEN("小黄");
Person jiaolian = new Person("叫练");
Person wife = new Person("叫练媳妇");
//创建线程执行下蛋和捡蛋的过程;
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
hen1.proEggs();
Thread.sleep(50);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
hen2.proEggs();
Thread.sleep(50);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//叫练捡鸡蛋的线程!
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
jiaolian.getEggs();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//叫练媳妇捡鸡蛋的线程!
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
wife.getEggs();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}

如上面代码,参照一对一生产和消费中wait/notify代码做了一些修改,创建了两个母鸡线程“小黑”,“小黄”,两个捡鸡蛋的线程“叫练”,“叫练媳妇”,执行结果是同步的,实现了多对多的生产和消费,如下图所示。有如下几点需要注意的地方:

  1. 鸡窝中能容纳最大的鸡蛋是10个。
  2. 下蛋proEggs()方法中判断鸡蛋数量是否大于等于10个使用的是while循环,wait收到通知,唤醒当前线程,需要重新判断一次,避免程序出现逻辑问题,这里不能用if,如果用if,程序可能出现EggsList有超过10以上鸡蛋的情况。这是这道程序中容易出现错误的地方,也是经常会被问到的点,值得重点探究下。
  3. 多对多的生产者和消费者。

母鸡下蛋实例:多线程通信生产者和消费者wait/notify和condition/await/signal条件队列

Lock条件队列


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; /**
* @author :jiaolian
* @date :Created in 2020-12-30 16:18
* @description:母鸡下蛋:多对多生产者和消费者 条件队列
* @modified By:
* 公众号:叫练
*/
public class MultCondition { private static Lock lock = new ReentrantLock();
//条件队列
private static Condition condition = lock.newCondition(); //装鸡蛋的容器
private static class EggsList {
private static final List<String> LIST = new ArrayList();
} //生产者:母鸡实体类
private static class HEN {
private String name; public HEN(String name) {
this.name = name;
} //下蛋
public void proEggs() {
try {
lock.lock();
while (EggsList.LIST.size() >= 10) {
condition.await();
}
//容器添加一个蛋
EggsList.LIST.add("1");
//鸡下蛋需要休息才能继续产蛋
Thread.sleep(1000);
System.out.println(name+":下了一个鸡蛋!共有"+ EggsList.LIST.size()+"个蛋");
//通知叫练/叫练媳妇捡蛋
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
} //人对象
private static class Person {
private String name; public Person(String name) {
this.name = name;
}
//取蛋
public void getEggs() throws InterruptedException {
try {
lock.lock();
while (EggsList.LIST.size() == 0) {
condition.await();
}
Thread.sleep(500);
EggsList.LIST.remove(0);
System.out.println(name+":从容器中捡出一个鸡蛋!还剩"+ EggsList.LIST.size()+"个蛋");
//通知叫练捡蛋
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
} public static void main(String[] args) {
//创造一个人和一只鸡
HEN hen1 = new HEN("小黑");
HEN hen2 = new HEN("小黄");
Person jiaolian = new Person("叫练");
Person wife = new Person("叫练媳妇");
//创建线程执行下蛋和捡蛋的过程;
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
hen1.proEggs();
Thread.sleep(50);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
hen2.proEggs();
Thread.sleep(50);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//叫练捡鸡蛋的线程!
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
jiaolian.getEggs();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//叫练媳妇捡鸡蛋的线程!
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
wife.getEggs();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}

如上面代码,只是将synchronized换成了Lock,程序运行的结果和上面的一致,下面我们比较下Lock和synchronized的异同。这个问题也是面试中会经常问到的!

Lock和synchronized比较


Lock和synchronized都能让多线程同步。主要异同点表现如下!

  1. 锁性质:Lock乐观锁是非阻塞的,底层是依赖cas+volatile实现,synchronized悲观锁是阻塞的,需要上下文切换。实现思想不一样。
  2. 功能细节上:Lock需要手动加解锁,synchronized自动加解锁。Lock还提供颗粒度更细的功能,比如tryLock等。
  3. 线程通信:Lock提供Condition条件队列,一把锁可以对应多个条件队列,对线程控制更细腻。synchronized只能对应一个wait/notify。

主要就这些吧,如果对synchronized,volatile,cas关键字不太了解的童鞋,可以看看我之前的文章,有很详细的案例和说明。

总结


今天用生活中的例子转化成代码,实现了两种多线程中消费者/生产者模式,给您的建议就是需要把代码敲一遍,如果认真执行了一遍代码应该能看明白,喜欢的请点赞加关注哦。我是叫练【公众号】,边叫边练。

母鸡下蛋实例:多线程通信生产者和消费者wait/notify和condition/await/signal条件队列

母鸡下蛋实例:多线程通信生产者和消费者wait/notify和condition/await/signal条件队列的更多相关文章

  1. JAVA之旅(十五)——多线程的生产者和消费者,停止线程&comma;守护线程&comma;线程的优先级,setPriority设置优先级,yield临时停止

    JAVA之旅(十五)--多线程的生产者和消费者,停止线程,守护线程,线程的优先级,setPriority设置优先级,yield临时停止 我们接着多线程讲 一.生产者和消费者 什么是生产者和消费者?我们 ...

  2. java 22 - 19 多线程之生产者和消费者的代码优化

    在之前,是把生产者录入数据和消费者获取数据的所有代码都分别写在各自的类中. 这样不大好 这次把生产者和消费者部分关键代码都写入资源类中: package zl_Thread; public class ...

  3. java 22 - 16 多线程之生产者和消费者的问题

    生产者和消费者问题的描述图 通过上图,我们可以发现: 生产者和消费者使用的都是同一个资源(肉包子) 所以,当使用线程的时候,这两类的锁也是同一把锁(为了避免出现线程安全问题) 例子:学生信息的录入和获 ...

  4. 多线程中Object的wait&lpar;&rpar;,notify&lpar;&rpar;和Condition的wait&lpar;&rpar;和singal&lpar;&rpar;对锁的关联

    通常将共享资源的操作放置在Sysnchronized定义的区域内,这样当其他线程也获取到这个锁时,必须的等待锁被释放时才能进入该区域.Object为任意一个对象,每个对象都存在一个标志位,并具有两个值 ...

  5. Java:多线程之生产者与消费者

    要求:用两个线程模拟存票.售票过程.但要求每存入一张票,就售出一张票,售出后,再存入,直到售完为止. 用到的知识点:线程等待.唤醒.可能的线程中断异常 下面的方式一和方式二采用的是唤醒所有等待的线程, ...

  6. 【Java多线程通信】syncrhoized下wait&lpar;&rpar;&sol;notify&lpar;&rpar;与ReentrantLock下condition的用法比较

    转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/6556925.html  一:syncrhoized使用同一把锁的多个线程用通信实现执行顺序的调度 我们知道,使 ...

  7. 【多线程】java多线程实现生产者消费者模式

    思考问题: 1.为什么用wait()+notify()实现生产者消费者模式? wait()方法可以暂停线程,并释放对象锁 notify()方法可以唤醒需要该对象锁的其他线程,并在执行完后续步骤,到了s ...

  8. Java多线程之生产者消费者问题&amp&semi;lt&semi;一&amp&semi;gt&semi;:使用synchronized keyword解决生产者消费者问题

    今天看了一片博文,讲Java多线程之线程的协作,当中作者用程序实例说明了生产者和消费者问题,但我及其它读者发现程序多跑几次还是会出现死锁,百度搜了下大都数的样例也都存在bug,经过细致研究发现当中的问 ...

  9. 第44天学习打卡(JUC 线程和进程 并发和并行 Lock锁 生产者和消费者问题 如何判断锁(8锁问题) 集合类不安全)

    什么是JUC 1.java.util工具包 包 分类 业务:普通的线程代码 Thread Runnable 没有返回值.效率相比Callable相对较低 2.线程和进程 进程:一个程序.QQ.exe, ...

随机推荐

  1. cinnamon桌面安装在其他目录下

    cinnamon桌面还不错,不过默认只能安装在/usr目录下 有很多脚本中写死了是/usr目录 编译时如下模块需要打补丁: 1.cinnamon中,需要执行 sed -i 's|usr/share|u ...

  2. eclipse debug时老提示edit source lookup path解决方案

    用myeclipse debug web应用的时候,总提示edit source lookup path,每次都得手动选择项目,费时费力.在网上终于找到了方法. 搬运:http://www.educi ...

  3. VersionCode和VersionName

    关于apk更新版本的问题   先上结论: Google为APK定义了两个关于版本属性:VersionCode和VersionName,他们有不同的用途. VersionCode:对消费者不可见,仅用于 ...

  4. 使用dom4j解析xml文件,并封装为javabean对象

    dom4j是一个java的XML api,性能优异.功能强大.易于使用.这里使用dom4j对xml文件进行解析,并完成对文件的封装. 实现对xml文件的解析,主要使用到的是dom4j中的SAXRead ...

  5. Sublime Text 3中配置运行Java

    1.安装JDK并配置环境变量 2.在JDK的bin目录下新建runJava.bat文件,右键选编辑,复制粘贴如下代码并保存: @echo off cd %~dp1 echo Compiling %~n ...

  6. think in uml-关系

    1.关联关系association 在一段时间内将多个类的实例连接在一起 某个对象在一段时间内一直"知道"另一个对象的存在 2.依赖关系dependency 一个对象的修改会导致另 ...

  7. Qt入门之基础篇&lpar;1&rpar;:Qt4及Qt5的下载与安装

    转载请注明出处:CN_Simo. 导语: Qt是一个跨平台的C++图形界面应用程序框架.它提供给开发者建立图形用户界面所需的功能,广泛用于开发GUI程序,也可用于开发非GUI程序.Qt很容易扩展,并且 ...

  8. java 信号量Semaphore

    Semaphore 信号量主要用于约束多个线程可同时获取的物理上的或者逻辑上的资源数.比如用在各种池的设计中. 信号量用于管理这些资源的一个虚拟的管理凭据.线程在获取一个资源时,首先要获取一个资源的许 ...

  9. sf-1 算法

    算法基础 算法 算法(Algorithm):一个计算过程,解决问题的方法 DNiklaus Wirth:“程序=数据结构+算法” 时间复杂度 时间复杂度:用来评估算法运行效率的一个式子 时间复杂度-小 ...

  10. Mysql索引基础原理

    索引的概念 索引是特殊数据结构:  定义在查找时作为查找条件的字段 索引实现在存储引擎 功能: 1.约束数据 2.加速查询 优点: 索引可以降低服务需要扫描的数据量,减少了IO次数 索引可以帮助服务器 ...