源码地址:https://github.com/nieandsun/concurrent-study.git


1 wait、notify、notifyAll简单介绍


1.1 使用方法 + 为什么不是Thread类的方法

为什么不是Thread类的方法

首先应该明确wait、notify、notifyAll三个方法都是对锁对象的操作,而锁可以是任何对象。在java的世界中任何对象都属于Object类,因此这三个方法都是Object的方法, 而不是线程对象Thread的方法。

使用方法

需要注意两点:

  • (1)这三个方法必须在synchronized关键字包含的临界区(简单理解,就是代码块)内使用
  • (2)使用方式为锁对象.方法(),比如obj.wait();

1.2 什么时候加锁、什么时候释放锁?

必须要明确以下几点:

  • (1)notify和notifyAll方法不会释放锁,这两个方法只是通知其他使用该锁当锁但是在wait状态的线程,可以准备抢锁了
    • 这里还要格外注意一点,其他使用该锁当锁且处于wait状态的线程只有被notify或notifyAll唤醒了,才有资格抢锁
  • (2)某个锁对象调用wait方法会立即释放当前线程的该对象锁 , 且其他线程通过notify/notifyAll方法通知该线程可以抢该对象锁时,如果当前线程抢到了,会从当前锁的wait方法之后开始执行 —- 即从哪里wait,从哪里执行;
  • (3)在synchronized、wait、notify、notifyAll的组合里
    • 加锁的方式只有一个即进入同步代码块时加锁;
    • 释放锁的方式有两个: ①锁对象调用wait方法时会释放锁 ;② 走完同步代码块时自动释放锁

1.3 notify、notifyAll的区别

  • 某个锁对象的notify只会唤醒一个使用该锁当锁且处于wait状态的线程;
  • 某个锁对象的notifyAll方法会把所有使用该锁当锁且处于wait状态的线程都唤醒;

使用建议: 为了防止某些线程无法被通知到,建议都使用notifyAll。


2 两个比较经典的使用案例

感觉上学的时候好像就考过下面这两个案例☺☺☺


2.1 案例1 —- ABCABC。。。三个线程顺序打印问题


2.1.1 题目

三个线程,线程A不停打印A、线程B不停的打印B、线程C不停的打印C,如何通过synchronized、wait、notifyAll(或notify)的组合,使三个线程不停地且顺序地打印出ABCABC。。。


2.1.2 题目分析

其实我在《【并发编程】—- Thread类中的join方法》这篇文章里用join实现过类似的功能,有兴趣的可以看一下。。。

如果使用synchronized、wait、notifyAll(或notify)的组合的话,这个问题可以归结为下图所示的问题。即:

线程A走完 ,线程B走 —-> 线程B走完,线程C走 —-》 线程C走完,线程A走 。。。。

【并发编程】 --- 线程间的通信wait、notify、notifyAll - 图1
以线程A为起点进行分析,可知:

  • (1)要想线程A走完,线程B接着走,那肯定是线程A释放了线程B所需要的锁,这里设该锁为U,做进一步分析可知:
    • 既然线程B需要线程A释放的锁U,那就意味着此时线程B中的锁U肯定处于wait状态;
    • 同时要想线程A释放了锁U之后,线程B可以被唤醒,线程A还必须得进行锁U的notify或notifyAll
  • (2)同理,要想线程B走完,线程C走,那肯定是线程C有一把处于wait状态的锁,这里设为V,需要线程B进行该锁的notify或notifyAll 并释放
  • (3)再同理,要想线程C走完,线程A接着走,那肯定是线程A有一把处于wait的锁,这里设为W,需要线程C进行该锁的notify或notifyAll 并释放

用图可以表示成下面的样子:

【并发编程】 --- 线程间的通信wait、notify、notifyAll - 图2
分析到这里我们可以再提炼一下:

  • (1)每个线程都应该有两把锁
  • (2)第一把锁是前面的线程释放后自己要抢到的锁、第二把锁是自己要notify或notifyAll的锁,对应到每个线程,就可以这样描述
    • 线程A需要两把锁,一把为线程C需要notify(或notifyAll)+ 释放的锁,可以认为该锁为C锁;另一把是自己需要notify(或notifyAll)+释放的锁,可以认为该锁为A锁
    • 同理,线程B需要A线程notify(或notifyAll)+ 释放的锁A锁,自己需要notify(或notifyAll)+释放的B锁
    • 再同理,线程C需要B线程notify(或notifyAll)+ 释放的锁B锁,自己需要notify(或notifyAll)+释放的C锁

分析到这里后,可以将上图改成下面的样子,这样理解起来,我感觉会更好一些:

【并发编程】 --- 线程间的通信wait、notify、notifyAll - 图3

分析到这里就可以写代码了。


2.1.3 我的答案

  • code
  1. package com.nrsc.ch1.base.producer_consumer.ABCABC;
  2. import lombok.AllArgsConstructor;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.junit.Test;
  5. @Slf4j
  6. @AllArgsConstructor
  7. public class ABCABC implements Runnable {
  8. private String obj;
  9. //前一个线程需要释放,本线程需要wait的锁
  10. private Object prev;
  11. //本线程需要释放,下一个线程需要wait的锁
  12. private Object self;
  13. @Override
  14. public void run() {
  15. int i = 3;
  16. while (i > 0) {
  17. //为了在控制台好看到效果,我这里打印3轮
  18. synchronized (prev) { //抢前面线程的锁
  19. synchronized (self) {// 抢到自己应该释放的锁
  20. System.out.println(obj);
  21. i--;
  22. self.notifyAll(); //唤醒其他线程抢self
  23. }//释放自己应该释放的锁
  24. try {
  25. //走到这里本线程已经释放了自己应该释放的锁,接下来就需要让自己需要等待的锁进行等待就可以了
  26. if (i > 0) { //我最开始没加这个条件,但是测试发现程序没停,其实分析一下就可以知道
  27. //当前面i--使i=0了,其实该线程就已经完成3次打印了,就不需要再等前面的锁了
  28. //因此这里加了该if判断
  29. prev.wait();
  30. }
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  36. }
  37. public static void main(String[] args) throws InterruptedException {
  38. Object lockA = new Object();
  39. Object lockB = new Object();
  40. Object lockC = new Object();
  41. //线程A需要等待C线程释放的锁,同时需要释放本线程该释放的锁A
  42. new Thread(new ABCABC("A", lockC, lockA)).start();
  43. Thread.sleep(1); //确保开始时A线程先执行
  44. //线程B需要等待A线程释放的锁,同时需要释放本线程该释放的锁B
  45. new Thread(new ABCABC("B", lockA, lockB)).start();
  46. Thread.sleep(1); //确保开始时B线程第2个执行
  47. //线程C需要等待B线程释放的锁,同时需要释放本线程该释放的锁C
  48. new Thread(new ABCABC("C", lockB, lockC)).start();
  49. }
  50. }
  • 测试结果:

【并发编程】 --- 线程间的通信wait、notify、notifyAll - 图4


2.2 生产者消费者问题


2.2.1 题目

如下图所示:

  • (1)有多个生产者,每个生产者都在不断的抢面包厂里的机器生产面包 —-> 某个时间段只能有一个生产者进行生产
  • (2)厂里最多能存储20箱,也就是说当已经有20箱了,各个生产者就不能生产了,需要等待消费者消费了,才能继续生产
  • (3)消费者也有多个,他们也会抢着去面包厂买面包,但也是某个时间段,只能有一个消费者抢到买面包的资格

在以上条件的基础上,写一个多线程程序,保证在生产者不断生产面包的同时,消费者也在不断的购买面包。
注意: 不能写成生产者先生产了20箱,然后消费者再去消费20箱)
【并发编程】 --- 线程间的通信wait、notify、notifyAll - 图5


2.2.2 题目分析

其实我觉得这个很简单,只需要想明白下面的两点肯定就可以把这个代码写出来。

对于生产者

  • (1)它们要不停地生产,直到面包的箱数大于等于20时,生产者就等待 —-> 等着消费者去消费
  • (2)当面包的箱数小于20时,抢到生产权的生产者就生产,并通知消费者,我刚生产了一个,你们可以再继续消费了

对于消费者

  • (1)他们要不停地消费,知道面包的箱数为0时,它们就等待 —-> 等着生产这去生产
  • (2)当面包的箱数大于0时,抢到消费权的消费者就消费,并通知生产者,我刚消费了一个,你们可以再继续生产了

2.2.3 我的答案

  • 生产者和消费者
  1. package com.nrsc.ch1.base.producer_consumer.multi;
  2. import lombok.extern.slf4j.Slf4j;
  3. @Slf4j
  4. public class BreadProducerAndConsumer2 {
  5. /***面包集合*/
  6. private int i = 0;
  7. /***
  8. * 生产者 ,注意这里锁是当前对象,即this
  9. */
  10. public synchronized void produceBread() {
  11. //如果大于等于20箱,就等待 --- 如果这里为大于20的话,则20不会进入while,则会生产出21箱,所以这里应为>=
  12. while (i >= 20) {
  13. try {
  14. this.wait();
  15. } catch (InterruptedException e) {
  16. log.error("生产者{},等待出错", Thread.currentThread().getName(), e);
  17. }
  18. }
  19. //如果不到20箱就继续生产
  20. i++; //生产一箱
  21. log.warn("{}生产一箱面包,现有面包{}个", Thread.currentThread().getName(), i);
  22. //生产完,通知消费者进行消费
  23. this.notifyAll();
  24. }
  25. /***
  26. * 消费者
  27. */
  28. public synchronized void consumeBread() {
  29. //如果没有了就等待
  30. while (i <= 0) {
  31. try {
  32. this.wait();
  33. } catch (InterruptedException e) {
  34. log.error("消费者{},等待出错", Thread.currentThread().getName(), e);
  35. }
  36. }
  37. //能走到这里说明i>0,所以进行消费
  38. i--; //消费一箱
  39. log.info("{}消费一个面包,现有面包{}个", Thread.currentThread().getName(), i);
  40. //消费完,通知生产者进行生产
  41. this.notifyAll();
  42. }
  43. }
  • 测试类
  1. package com.nrsc.ch1.base.producer_consumer.multi;
  2. public class MultiTest {
  3. public static void main(String[] args) throws InterruptedException {
  4. BreadProducerAndConsumer2 pc = new BreadProducerAndConsumer2();
  5. /***
  6. * 不睡眠几秒,效果不是很好,
  7. * 因此我在
  8. * 生产者线程里睡了12秒 --- 因为我觉得生产面包的时间应该长 ☻☻☻
  9. * 消费者线程里睡了6秒 --- 因为我觉得买面包的时间应该快 ☻☻☻
  10. */
  11. //生产者线程
  12. for (int i = 0; i < 6; i++) {
  13. new Thread(() -> {
  14. //每个线程都不停的生产
  15. while (true) {
  16. try {
  17. Thread.sleep(12);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. pc.produceBread();
  22. }
  23. }, "生产者" + i).start();
  24. }
  25. //消费者线程
  26. for (int i = 0; i < 6; i++) {
  27. new Thread(() -> {
  28. //每个线程都不停的消费
  29. while (true) {
  30. try {
  31. Thread.sleep(6);
  32. } catch (InterruptedException e) {
  33. e.printStackTrace();
  34. }
  35. pc.consumeBread();
  36. }
  37. }, "消费者" + i).start();
  38. }
  39. }
  40. }
  • 测试效果如下:

【并发编程】 --- 线程间的通信wait、notify、notifyAll - 图6