5.2线程通信的设计思想
目录介绍
- 01.线程通信的背景
- 1.1 先思考一下问题
- 1.2 线程通信概念
- 1.3 设计通信的思想
- 02.线程通信的方式
- 2.1 通过需求实现案例
- 2.2 wait和notify方式
- 2.3 Condition休眠和唤醒
- 2.4 CountDownLatch方式
- 03.wait和notify
- 3.1 wait和notifyAll实践
- 3.2 wait虚假唤醒问题
- 3.3 解决wait虚假唤醒
- 3.4 替换notify出现死锁
- 3.5 搞清楚锁池和等待池
- 3.6 notify和notifyAll
- 3.7 notify死锁解决方法
- 04.Condition设计思想
- 4.1
- 05.CountDownLatch实践
- 5.1
- 07.高级并发工具类
参考博客
- https://www.51cto.com/article/750927.html
01.线程通信的背景
1.1 先思考一下问题
- 什么是线程通信,有什么作用
- 线程通信的三种实现方式
- notifyAll的虚假唤醒问题,notify死锁问题
- 通过 ReentrantLock 实现精确唤醒
- 多线程按顺序执行的四种方案
1.2 线程通信概念
- 线程通信是指线程之间通过某种机制传递信息,以协调它们之间的行为。
- Java 中提供了内置的线程通信机制,包括 wait()、notify() 和 notifyAll() 方法,这些方法都是 Object 类的一部分,每个对象都可以作为线程通信的锁。
1.3 设计通信的思想
- 在Java中,线程通信的思想主要基于两个关键概念:共享对象和同步机制。以下是一些常见的线程通信思想和机制:
02.线程通信的方式
2.1 通过需求实现案例
- 比如:小明放假在家,肚子饿了,如果发现没有吃的就会喊:妈,我饿了,弄点吃的,如果妈妈发现没有吃的了就会做菜,通知小明吃饭,总之:有菜通知小明吃饭,没菜小明通知妈妈做饭,简直吃货一个。
- 此时就是两个线程对饭菜这同一资源有不同的任务,妈妈线程就是做饭,小明线程是吃饭,如果想要实现上边的场景,就需要妈妈线程和小明线程之间通信。
2.2 wait和notify方式
- Object类中相关的方法有notify方法和wait方法。因为wait和notify方法定义在Object类中,因此会被所有的类所继承。这些方法都是final的,即它们都是不能被重写的,不能通过子类覆写去改变它们的行为。
- ①wait()方法: 让当前线程进入等待,并释放锁。
- ②wait(long)方法: 让当前线程进入等待,并释放锁,不过等待时间为long,超过这个时间没有对当前线程进行唤醒,将自动唤醒。
- ③notify()方法: 让当前线程通知那些处于等待状态的线程,当前线程执行完毕后释放锁,并从其他线程中唤醒其中一个继续执行。
- ④notifyAll()方法: 让当前线程通知那些处于等待状态的线程,当前线程执行完毕后释放锁,将唤醒所有等待状态的线程。
2.3 Condition休眠和唤醒
- 关键字synchronized与wait()和notify()/notifyAll()方法相结合可以实现等待/通知模式,类似ReentrantLock也可以实现同样的功能,但需要借助于Condition对象。
- 关于Condition实现等待/通知就不详细介绍了,可以完全类比wait()/notify(),基本使用和注意事项完全一致。
- 就只简单介绍下类比情况:
- condition.await()————>lock.wait()
- condition.await(long time, TimeUnit unit)————>lock.wait(long timeout)
- condition.signal()————>lock.notify()
- condition.signaAll()————>lock.notifyAll()
2.4 CountDownLatch方式
- 使用JUC中的CountDownLatch【计数器】
03.wait和notify
3.1 wait和notifyAll实践
- 在此案例中,同一资源就是饭菜,小明对吃的操作是造,而妈妈对吃的操作是做。
public class WaitNotifyAllTest { public static void main(String[] args) { // 创建饭菜对象 KitChenRoom chenRoom = new KitChenRoom(); // 创建小明妈妈线程,做饭 new Thread(() -> { for (int i = 0; i < 3; i++) { chenRoom.cook(); } },"小明妈妈线程:").start(); // 创建小明自己线程,吃饭 new Thread(() -> { for (int i = 0; i < 3; i++) { chenRoom.eat(); } },"小明自己线程:").start(); } public static class KitChenRoom { // 是否有吃的 private boolean hasFood = false; // 设置同步锁,做饭和吃饭只能同时有一个在执行,不能边做边吃 private final Object lock = new Object(); // 做饭 public void cook() { // 加锁 synchronized (lock) { // 如果有吃的,就不做饭 if(hasFood) { // 还有吃的,先不做饭 try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 否则就做饭, System.out.println(Thread.currentThread().getName() + "没吃的了,给娃做饭!"); // 做好之后,修改为true hasFood = true; // 通知其他线程吃饭 lock.notifyAll(); } } // 吃饭 public void eat() { synchronized (lock) { // 如果没吃的,就喊妈妈做饭,暂时吃不了 if (!hasFood) { try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 否则就吃饭 System.out.println(Thread.currentThread().getName() + "感谢老妈,恰饭,恰饭"); // 吃完之后,修改为false hasFood = false; // 通知其他线程吃饭 lock.notifyAll(); } } } }
- 运行结果:发现两个线程交替执行,没饭的时候妈妈做饭,有饭的时候小明就恰饭
小明妈妈线程:没吃的了,给娃做饭! 小明自己线程:感谢老妈,恰饭,恰饭 小明妈妈线程:没吃的了,给娃做饭! 小明自己线程:感谢老妈,恰饭,恰饭 小明妈妈线程:没吃的了,给娃做饭! 小明自己线程:感谢老妈,恰饭,恰饭
3.2 wait虚假唤醒问题
- 在wait方法的源码注释中有这么一段话:
- As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop
- 翻译:在单参数版本中,中断和虚假唤醒是可能的,并且该方法应始终在循环中使用
- 现在业务场景添加:如果此时我们再开启一个大明线程吃饭,开启一个爸爸线程做饭,此时会发生什么问题呢。
- 改造测试类:再开启一个大明线程和一个爸爸线程。
public class WaitNotifyAllTest2 { public static void main(String[] args) { // 创建饭菜对象 KitChenRoom chenRoom = new KitChenRoom(); // 创建小明妈妈线程,做饭 new Thread(() -> { for (int i = 0; i < 3; i++) { chenRoom.cook(); } },"小明妈妈线程:").start(); // 爸爸线程:做饭 new Thread(() -> { for (int i = 0; i < 3; i++) { chenRoom.cook(); } },"小明爸爸线程:").start(); // 创建小明自己线程,吃饭 new Thread(() -> { for (int i = 0; i < 3; i++) { chenRoom.eat(); } },"小明自己线程:").start(); // 大明线程:吃饭 new Thread(() -> { for (int i = 0; i < 3; i++) { chenRoom.eat(); } },"大明哥哥线程:").start(); } public static class KitChenRoom { // 是否有吃的 private boolean hasFood = false; // 设置同步锁,做饭和吃饭只能同时有一个在执行,不能边做边吃 private final Object lock = new Object(); // 做饭 public void cook() { // 加锁 synchronized (lock) { // 如果有吃的,就不做饭 if(hasFood) { // 还有吃的,先不做饭 try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 否则就做饭, System.out.println(Thread.currentThread().getName() + "没吃的了,给娃做饭!"); // 做好之后,修改为true hasFood = true; // 通知其他线程吃饭 lock.notifyAll(); } } // 吃饭 public void eat() { synchronized (lock) { // 如果没吃的,就喊妈妈做饭,暂时吃不了 if (!hasFood) { try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 否则就吃饭 System.out.println(Thread.currentThread().getName() + "感谢老妈,恰饭,恰饭"); // 吃完之后,修改为false hasFood = false; // 通知其他线程吃饭 lock.notifyAll(); } } } }
- 运行结果:发现爸爸线程和妈妈线程连着做了三次饭
小明妈妈线程:没吃的了,给娃做饭! 小明自己线程:感谢老妈,恰饭,恰饭 小明爸爸线程:没吃的了,给娃做饭! 小明妈妈线程:没吃的了,给娃做饭! 小明爸爸线程:没吃的了,给娃做饭! 小明自己线程:感谢老妈,恰饭,恰饭 小明爸爸线程:没吃的了,给娃做饭! 小明妈妈线程:没吃的了,给娃做饭! 小明自己线程:感谢老妈,恰饭,恰饭
- 为什么会出现这种情况呢?分析一下:
- 这是由于wait方法的机制导致的,wait方法会使线程阻塞,直到被唤醒之后才会运行,在哪里阻塞,再次被唤醒之后得到CPU执行权,就会在哪里继续运行。
- 现在是4条线程,假设爸爸线程运行之后将 hasFood 改为true,此时爸爸线程就会唤醒其他线程,也就是妈妈线程和小明,大明线程都会被唤醒,如果此时妈妈线程获取到CPU时间片开始运行,判断 hasFood 为 true,那么就触发wait等待,等待之后就会释放CPU执行权,唤醒其他线程。
- 如果此时爸爸线程又获取到CPU执行权,同样判断hasFood之后为true,就会进入等待,唤醒其他线程,如果此时CPU执行权又分配给了妈妈线程,因为之前已经经过了判断,就会在wait的地方,继续执行,就会触发给娃做饭,之后再唤醒其他线程。
- 此时爸爸线程得到CPU时间片,则会在上次wait的地方继续执行,同样的给娃做饭,就会出现上图的效果,爸妈线程交替做饭。
image1
3.3 解决wait虚假唤醒
- 解决3.2问题方案:
- 将if替换为while,while语句块每次执行完之后都会重新判断,知道条件不成立才会结束循环,即可解决。
public static class KitChenRoom { // 是否有吃的 private boolean hasFood = false; // 设置同步锁,做饭和吃饭只能同时有一个在执行,不能边做边吃 private final Object lock = new Object(); // 做饭 public void cook() { // 加锁 synchronized (lock) { // 如果有吃的,就不做饭 while(hasFood) { // 还有吃的,先不做饭 try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 否则就做饭, System.out.println(Thread.currentThread().getName() + "没吃的了,给娃做饭!"); // 做好之后,修改为true hasFood = true; // 通知其他线程吃饭 lock.notifyAll(); } } // 吃饭 public void eat() { synchronized (lock) { // 如果没吃的,就喊妈妈做饭,暂时吃不了 while (!hasFood) { try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 否则就吃饭 System.out.println(Thread.currentThread().getName() + "感谢老妈,恰饭,恰饭"); // 吃完之后,修改为false hasFood = false; // 通知其他线程吃饭 lock.notifyAll(); } } }
- 运行结果:发现做饭和吃饭交替执行
小明妈妈线程:没吃的了,给娃做饭! 小明自己线程:感谢老妈,恰饭,恰饭 小明爸爸线程:没吃的了,给娃做饭! 小明自己线程:感谢老妈,恰饭,恰饭 小明妈妈线程:没吃的了,给娃做饭! 小明自己线程:感谢老妈,恰饭,恰饭 小明爸爸线程:没吃的了,给娃做饭! 大明哥哥线程:感谢老妈,恰饭,恰饭 小明妈妈线程:没吃的了,给娃做饭! 大明哥哥线程:感谢老妈,恰饭,恰饭 小明爸爸线程:没吃的了,给娃做饭! 大明哥哥线程:感谢老妈,恰饭,恰饭
- 为什么使用while就能解决呢?其实就是 if和while的区别
- 由于在多线程内容中,有很多小伙伴犯迷,为什么用while就解决了,其实是思路没有打开,把以前学的东西都忘记了,满脑子都是多线程的东西,你说是不是!学习要融会贯通,将前后所有的知识点串起来。
- 解决虚假唤醒非常简单,其实就是利用了while的特性,while体每次执行都会循环再次判断条件,直到条件不成立跳出循环,在这也是一样:
- 妈妈线程执行发现hasFood = true,就进入等待,再次得到cpu时间片执行时,在哪里等待就在哪里醒来继续执行,也就是再lock.wait()的地方继续执行
- 由于该代码在while循环中,会循环判断,如果hasFood = true继续wait,如果hasFood = false就跳出循环,执行循环体之外的代码
- 但是如果是if,就只会判断一次,醒来之后不会再次判断,因为lock.wait()代码已经执行过了,会直接向下执行,开始给娃做饭
3.4 替换notify出现死锁
- 上边我们使用notifyAll唤醒了所有线程,如果将notifyAll替换为notify会发生什么?
public static class KitChenRoom { // 是否有吃的 private boolean hasFood = false; // 设置同步锁,做饭和吃饭只能同时有一个在执行,不能边做边吃 private final Object lock = new Object(); // 做饭 public void cook() { // 加锁 synchronized (lock) { // 如果有吃的,就不做饭 while(hasFood) { // 还有吃的,先不做饭 try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 否则就做饭, System.out.println(Thread.currentThread().getName() + "没吃的了,给娃做饭!"); // 做好之后,修改为true hasFood = true; // 通知其他线程吃饭 lock.notify(); } } // 吃饭 public void eat() { synchronized (lock) { // 如果没吃的,就喊妈妈做饭,暂时吃不了 while (!hasFood) { try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 否则就吃饭 System.out.println(Thread.currentThread().getName() + "感谢老妈,恰饭,恰饭"); // 吃完之后,修改为false hasFood = false; // 通知其他线程吃饭 lock.notify(); } } }
- 运行结果:多运行几次,发现程序卡住不动,产生死锁。
小明妈妈线程:没吃的了,给娃做饭! 小明自己线程:感谢老妈,恰饭,恰饭 小明妈妈线程:没吃的了,给娃做饭! 大明哥哥线程:感谢老妈,恰饭,恰饭
3.5 搞清楚锁池和等待池
- 在解释3.4案例使用notify导致死锁这个原因之前,先搞清楚 锁池 和 等待池 两个概念:
- 锁池:假设线程A已经拥有了某个对象的锁【注意:不是类】,而其它的线程想要调用这个对象的某个synchronized方法【或者synchronized块】,由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
- 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,之后进入到了该对象的等待池中。
- 对象锁:任何一个对象都可以被当做锁,所以称为对象锁,比如下方代码lock1和lock2就是两把对象锁,都有自己独立的锁池和等待池。
- 调用 lock1.wait() 就是该线程进入到lock1对象锁的等待池中
- lock1.notify()就是唤醒lock1对象锁的等待池中的随机一个等待线程,lock1.notifyAll(); 就是唤醒该等待池中所有等待线程
- lock1的锁池和等待池与lock2是独立的,互不影响,并不会唤醒彼此等待池中的线程
3.6 notify和notifyAll
- 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
- 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
- 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
3.7 notify死锁解决方法
- 使用notify:notify方法只能唤醒一个线程,其它等待的线程仍然处于wait状态。
- 假设调用cook方法的线程执行完后,所有的线程都处于等待状态,此时又执行了notify方法,这时如果唤醒的仍然是一个调用cook方法的线程【比如爸爸线程 将 妈妈线程唤醒】,那么while循环等于true,则此唤醒的线程【妈妈线程】就会调用wait方法,也会处于等待状态,而且没有唤醒其他线程,此时所有的线程都处于等待状态,就发生了死锁。
- 使用notifyAll:可以唤醒所有正在等待该锁的线程。
- 那么所有的线程都会处于运行前的准备状态(就是cook方法执行完后,唤醒了所有等待该锁的线程),那么此时,即使再次唤醒一个调用cook方法的线程,while循环等于true,唤醒的线程再次处于等待状态,那么还会有其它的线程可以获得锁,进入运行状态。
- 解决wait死锁的两种方案:
- 通过调用notifyAll唤醒所有等待线程。这种方式可以看3.1 wait和notifyAll实践
- 调用 wait(long timeout) 重载方法,设置等待超时时长,在指定时间内还没被唤醒则自动醒来。经过下面案例测试,运行结果:运行三次发现,第一次程序陷入了两次等待2秒之后程序继续执行,这就是超时自动唤醒,避免了死锁。
public static class KitChenRoom { // 是否有吃的 private boolean hasFood = false; // 设置同步锁,做饭和吃饭只能同时有一个在执行,不能边做边吃 private final Object lock = new Object(); // 做饭 public void cook() { // 加锁 synchronized (lock) { // 如果有吃的,就不做饭 while(hasFood) { // 还有吃的,先不做饭 try { lock.wait(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 否则就做饭, System.out.println(Thread.currentThread().getName() + "没吃的了,给娃做饭!"); // 做好之后,修改为true hasFood = true; // 通知其他线程吃饭 lock.notify(); } } // 吃饭 public void eat() { synchronized (lock) { // 如果没吃的,就喊妈妈做饭,暂时吃不了 while (!hasFood) { try { lock.wait(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } } // 否则就吃饭 System.out.println(Thread.currentThread().getName() + "感谢老妈,恰饭,恰饭"); // 吃完之后,修改为false hasFood = false; // 通知其他线程吃饭 lock.notify(); } } }
- notify死锁解决方法总结一下
- notify方法很容易引起死锁,除非你根据自己的程序设计,确定不会发生死锁,notifyAll方法则是线程的安全唤醒方法。
02.Condition实现等待/通知
2.1 Condition简单介绍
2.2 Condition实现方式
- 特殊之处:synchronized相当于整个ReentrantLock对象只有一个单一的Condition对象情况。而一个ReentrantLock却可以拥有多个Condition对象,来实现通知部分线程。
- 具体实现方式:
- 假设有两个Condition对象:ConditionA和ConditionB。那么由ConditionA.await()方法进入等待状态的线程,由ConditionA.signalAll()通知唤醒;由ConditionB.await()方法进入等待状态的线程,由ConditionB.signalAll()通知唤醒。篇幅有限,代码示例就不写了。