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

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


  1. package com.duyang.thread.basic.waitLock.demo;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. /**
  5. * @author :jiaolian
  6. * @date :Created in 2020-12-30 16:18
  7. * @description:母鸡下蛋:一对一生产者和消费者
  8. * @modified By:
  9. * 公众号:叫练
  10. */
  11. public class SingleNotifyWait {
  12. //装鸡蛋的容器
  13. private static class EggsList {
  14. private static final List<String> LIST = new ArrayList();
  15. }
  16. //生产者:母鸡实体类
  17. private static class HEN {
  18. private String name;
  19. public HEN(String name) {
  20. this.name = name;
  21. }
  22. //下蛋
  23. public void proEggs() throws InterruptedException {
  24. synchronized (EggsList.class) {
  25. if (EggsList.LIST.size() == 1) {
  26. EggsList.class.wait();
  27. }
  28. //容器添加一个蛋
  29. EggsList.LIST.add("1");
  30. //鸡下蛋需要休息才能继续产蛋
  31. Thread.sleep(1000);
  32. System.out.println(name+":下了一个鸡蛋!");
  33. //通知叫练捡蛋
  34. EggsList.class.notify();
  35. }
  36. }
  37. }
  38. //人对象
  39. private static class Person {
  40. private String name;
  41. public Person(String name) {
  42. this.name = name;
  43. }
  44. //取蛋
  45. public void getEggs() throws InterruptedException {
  46. synchronized (EggsList.class) {
  47. if (EggsList.LIST.size() == 0) {
  48. EggsList.class.wait();
  49. }
  50. Thread.sleep(500);
  51. EggsList.LIST.remove(0);
  52. System.out.println(name+":从容器中捡出一个鸡蛋");
  53. //通知叫练捡蛋
  54. EggsList.class.notify();
  55. }
  56. }
  57. }
  58. public static void main(String[] args) {
  59. //创造一个人和一只鸡
  60. HEN hen = new HEN("小黑");
  61. Person person = new Person("叫练");
  62. //创建线程执行下蛋和捡蛋的过程;
  63. new Thread(()->{
  64. try {
  65. for (int i=0; i<Integer.MAX_VALUE;i++) {
  66. hen.proEggs();
  67. }
  68. } catch (InterruptedException e) {
  69. e.printStackTrace();
  70. }
  71. }).start();
  72. //叫练捡鸡蛋的过程!
  73. new Thread(()->{
  74. try {
  75. for (int i=0; i<Integer.MAX_VALUE;i++) {
  76. person.getEggs();
  77. }
  78. } catch (InterruptedException e) {
  79. e.printStackTrace();
  80. }
  81. }).start();
  82. }
  83. }

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

  1. package com.duyang.thread.basic.waitLock.demo;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. import java.util.concurrent.locks.Condition;
  5. import java.util.concurrent.locks.Lock;
  6. import java.util.concurrent.locks.ReentrantLock;
  7. /**
  8. * @author :jiaolian
  9. * @date :Created in 2020-12-30 16:18
  10. * @description:母鸡下蛋:一对一生产者和消费者 条件队列
  11. * @modified By:
  12. * 公众号:叫练
  13. */
  14. public class SingleCondition {
  15. private static Lock lock = new ReentrantLock();
  16. //条件队列
  17. private static Condition condition = lock.newCondition();
  18. //装鸡蛋的容器
  19. private static class EggsList {
  20. private static final List<String> LIST = new ArrayList();
  21. }
  22. //生产者:母鸡实体类
  23. private static class HEN {
  24. private String name;
  25. public HEN(String name) {
  26. this.name = name;
  27. }
  28. //下蛋
  29. public void proEggs() {
  30. try {
  31. lock.lock();
  32. if (EggsList.LIST.size() == 1) {
  33. condition.await();
  34. }
  35. //容器添加一个蛋
  36. EggsList.LIST.add("1");
  37. //鸡下蛋需要休息才能继续产蛋
  38. Thread.sleep(1000);
  39. System.out.println(name+":下了一个鸡蛋!");
  40. //通知叫练捡蛋
  41. condition.signal();
  42. } catch (Exception e) {
  43. e.printStackTrace();
  44. } finally {
  45. lock.unlock();
  46. }
  47. }
  48. }
  49. //人对象
  50. private static class Person {
  51. private String name;
  52. public Person(String name) {
  53. this.name = name;
  54. }
  55. //取蛋
  56. public void getEggs() {
  57. try {
  58. lock.lock();
  59. if (EggsList.LIST.size() == 0) {
  60. condition.await();
  61. }
  62. Thread.sleep(500);
  63. EggsList.LIST.remove(0);
  64. System.out.println(name+":从容器中捡出一个鸡蛋");
  65. //通知叫练捡蛋
  66. condition.signal();
  67. } catch (Exception e) {
  68. e.printStackTrace();
  69. } finally {
  70. lock.unlock();
  71. }
  72. }
  73. }
  74. public static void main(String[] args) {
  75. //创造一个人和一只鸡
  76. HEN hen = new HEN("小黑");
  77. Person person = new Person("叫练");
  78. //创建线程执行下蛋和捡蛋的过程;
  79. new Thread(()->{
  80. for (int i=0; i<Integer.MAX_VALUE;i++) {
  81. hen.proEggs();
  82. }
  83. }).start();
  84. //叫练捡鸡蛋的过程!
  85. new Thread(()->{
  86. for (int i=0; i<Integer.MAX_VALUE;i++) {
  87. person.getEggs();
  88. }
  89. }).start();
  90. }
  91. }

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


  1. package com.duyang.thread.basic.waitLock.demo;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. /**
  5. * @author :jiaolian
  6. * @date :Created in 2020-12-30 16:18
  7. * @description:母鸡下蛋:多对多生产者和消费者
  8. * @modified By:
  9. * 公众号:叫练
  10. */
  11. public class MultNotifyWait {
  12. //装鸡蛋的容器
  13. private static class EggsList {
  14. private static final List<String> LIST = new ArrayList();
  15. }
  16. //生产者:母鸡实体类
  17. private static class HEN {
  18. private String name;
  19. public HEN(String name) {
  20. this.name = name;
  21. }
  22. //下蛋
  23. public void proEggs() throws InterruptedException {
  24. synchronized (EggsList.class) {
  25. while (EggsList.LIST.size() >= 10) {
  26. EggsList.class.wait();
  27. }
  28. //容器添加一个蛋
  29. EggsList.LIST.add("1");
  30. //鸡下蛋需要休息才能继续产蛋
  31. Thread.sleep(1000);
  32. System.out.println(name+":下了一个鸡蛋!共有"+EggsList.LIST.size()+"个蛋");
  33. //通知叫练捡蛋
  34. EggsList.class.notify();
  35. }
  36. }
  37. }
  38. //人对象
  39. private static class Person {
  40. private String name;
  41. public Person(String name) {
  42. this.name = name;
  43. }
  44. //取蛋
  45. public void getEggs() throws InterruptedException {
  46. synchronized (EggsList.class) {
  47. while (EggsList.LIST.size() == 0) {
  48. EggsList.class.wait();
  49. }
  50. Thread.sleep(500);
  51. EggsList.LIST.remove(0);
  52. System.out.println(name+":从容器中捡出一个鸡蛋!还剩"+EggsList.LIST.size()+"个蛋");
  53. //通知叫练捡蛋
  54. EggsList.class.notify();
  55. }
  56. }
  57. }
  58. public static void main(String[] args) {
  59. //创造一个人和一只鸡
  60. HEN hen1 = new HEN("小黑");
  61. HEN hen2 = new HEN("小黄");
  62. Person jiaolian = new Person("叫练");
  63. Person wife = new Person("叫练媳妇");
  64. //创建线程执行下蛋和捡蛋的过程;
  65. new Thread(()->{
  66. try {
  67. for (int i=0; i<Integer.MAX_VALUE;i++) {
  68. hen1.proEggs();
  69. Thread.sleep(50);
  70. }
  71. } catch (InterruptedException e) {
  72. e.printStackTrace();
  73. }
  74. }).start();
  75. new Thread(()->{
  76. try {
  77. for (int i=0; i<Integer.MAX_VALUE;i++) {
  78. hen2.proEggs();
  79. Thread.sleep(50);
  80. }
  81. } catch (InterruptedException e) {
  82. e.printStackTrace();
  83. }
  84. }).start();
  85. //叫练捡鸡蛋的线程!
  86. new Thread(()->{
  87. try {
  88. for (int i=0; i<Integer.MAX_VALUE;i++) {
  89. jiaolian.getEggs();
  90. }
  91. } catch (InterruptedException e) {
  92. e.printStackTrace();
  93. }
  94. }).start();
  95. //叫练媳妇捡鸡蛋的线程!
  96. new Thread(()->{
  97. try {
  98. for (int i=0; i<Integer.MAX_VALUE;i++) {
  99. wife.getEggs();
  100. }
  101. } catch (InterruptedException e) {
  102. e.printStackTrace();
  103. }
  104. }).start();
  105. }
  106. }

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

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

image.png

  1. import java.util.ArrayList;
  2. import java.util.List;
  3. import java.util.concurrent.locks.Condition;
  4. import java.util.concurrent.locks.Lock;
  5. import java.util.concurrent.locks.ReentrantLock;
  6. /**
  7. * @author :jiaolian
  8. * @date :Created in 2020-12-30 16:18
  9. * @description:母鸡下蛋:多对多生产者和消费者 条件队列
  10. * @modified By:
  11. * 公众号:叫练
  12. */
  13. public class MultCondition {
  14. private static Lock lock = new ReentrantLock();
  15. //条件队列
  16. private static Condition condition = lock.newCondition();
  17. //装鸡蛋的容器
  18. private static class EggsList {
  19. private static final List<String> LIST = new ArrayList();
  20. }
  21. //生产者:母鸡实体类
  22. private static class HEN {
  23. private String name;
  24. public HEN(String name) {
  25. this.name = name;
  26. }
  27. //下蛋
  28. public void proEggs() {
  29. try {
  30. lock.lock();
  31. while (EggsList.LIST.size() >= 10) {
  32. condition.await();
  33. }
  34. //容器添加一个蛋
  35. EggsList.LIST.add("1");
  36. //鸡下蛋需要休息才能继续产蛋
  37. Thread.sleep(1000);
  38. System.out.println(name+":下了一个鸡蛋!共有"+ EggsList.LIST.size()+"个蛋");
  39. //通知叫练/叫练媳妇捡蛋
  40. condition.signalAll();
  41. } catch (Exception e) {
  42. e.printStackTrace();
  43. } finally {
  44. lock.unlock();
  45. }
  46. }
  47. }
  48. //人对象
  49. private static class Person {
  50. private String name;
  51. public Person(String name) {
  52. this.name = name;
  53. }
  54. //取蛋
  55. public void getEggs() throws InterruptedException {
  56. try {
  57. lock.lock();
  58. while (EggsList.LIST.size() == 0) {
  59. condition.await();
  60. }
  61. Thread.sleep(500);
  62. EggsList.LIST.remove(0);
  63. System.out.println(name+":从容器中捡出一个鸡蛋!还剩"+ EggsList.LIST.size()+"个蛋");
  64. //通知叫练捡蛋
  65. condition.signalAll();
  66. } catch (Exception e) {
  67. e.printStackTrace();
  68. } finally {
  69. lock.unlock();
  70. }
  71. }
  72. }
  73. public static void main(String[] args) {
  74. //创造一个人和一只鸡
  75. HEN hen1 = new HEN("小黑");
  76. HEN hen2 = new HEN("小黄");
  77. Person jiaolian = new Person("叫练");
  78. Person wife = new Person("叫练媳妇");
  79. //创建线程执行下蛋和捡蛋的过程;
  80. new Thread(()->{
  81. try {
  82. for (int i=0; i<Integer.MAX_VALUE;i++) {
  83. hen1.proEggs();
  84. Thread.sleep(50);
  85. }
  86. } catch (InterruptedException e) {
  87. e.printStackTrace();
  88. }
  89. }).start();
  90. new Thread(()->{
  91. try {
  92. for (int i=0; i<Integer.MAX_VALUE;i++) {
  93. hen2.proEggs();
  94. Thread.sleep(50);
  95. }
  96. } catch (InterruptedException e) {
  97. e.printStackTrace();
  98. }
  99. }).start();
  100. //叫练捡鸡蛋的线程!
  101. new Thread(()->{
  102. try {
  103. for (int i=0; i<Integer.MAX_VALUE;i++) {
  104. jiaolian.getEggs();
  105. }
  106. } catch (InterruptedException e) {
  107. e.printStackTrace();
  108. }
  109. }).start();
  110. //叫练媳妇捡鸡蛋的线程!
  111. new Thread(()->{
  112. try {
  113. for (int i=0; i<Integer.MAX_VALUE;i++) {
  114. wife.getEggs();
  115. }
  116. } catch (InterruptedException e) {
  117. e.printStackTrace();
  118. }
  119. }).start();
  120. }
  121. }

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


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

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

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


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

版权声明:本文为jiaolian原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/jiaolian/p/14214842.html