当前位置:   article > 正文

Java 多线程 —— wait 与 notify_for countdownlatch wait

for countdownlatch wait

引言

认识一下 Object 类中的两个和多线程有关的方法:wait 和 notify。

wait,当前线程进入 WAITING 状态,释放锁资源

notify,唤醒等待中的线程,不释放锁资源

一、使用 wait-notify 实现一个监控程序

实现一个容器报警功能,两个线程分别执行以下任务:

t1 为容器添加10个元素;

t2实时监控容器中元素的个数,当个数为5时,线程2给出提示并结束。

1.1 简单的 while-true 版本

  1. /**
  2. * 一个普通的容器
  3. */
  4. class Container {
  5. private volatile List<Object> values = new ArrayList<>();
  6. public void add(Object value) {
  7. values.add(value);
  8. }
  9. public Integer sise() {
  10. return values.size();
  11. }
  12. }
  1. public class T_Alert01 {
  2. public static void main(String[] args) {
  3. Container container = new Container();
  4. new Thread(() -> {
  5. while (true) {
  6. if (container.sise() == 5) {
  7. System.out.println("alert 5!");
  8. break;
  9. }
  10. }
  11. }, "T2").start();
  12. new Thread(() -> {
  13. for (int i = 0; i < 10; i++) {
  14. container.add(new Object());
  15. System.out.println("已添加:" + container.sise());
  16. }
  17. }, "T1").start();
  18. }
  19. }

程序解析:监控线程 T2 通过 while-true 监控容器内的元素数量,但当 size == 5 时,还未来得及报警,T1 就又添加了多个元素;而且 while-true 会浪费很多CPU 资源。

显然,这么做无法满足我们的要求。

1.2 wait-notify 初版

  1. public class T_Alert02 {
  2. public static void main(String[] args) throws InterruptedException {
  3. Container container = new Container();
  4. final Object lock = new Object();
  5. new Thread(() -> {
  6. synchronized (lock) {
  7. if (container.sise() != 5) {
  8. try {
  9. lock.wait();
  10. } catch (InterruptedException e) {
  11. }
  12. }
  13. System.out.println("alert 5!");
  14. }
  15. }, "T2").start();
  16. TimeUnit.SECONDS.sleep(1);
  17. new Thread(() -> {
  18. synchronized (lock) {
  19. for (int i = 0; i < 10; i++) {
  20. container.add(new Object());
  21. System.out.println("已添加:" + container.sise());
  22. if (container.sise() == 5) {
  23. lock.notify();
  24. }
  25. }
  26. }
  27. }, "T1").start();
  28. }
  29. }

程序解析:T2监控线程先去判断容器大小,如果未达到报警标准,则等待在 lock 对象上,WAITING 是一种挂起状态,不消耗CPU资源。

T1 线程在达到 5 个时,触发一个 notify方法,唤醒其他等待中的线程。然而,仅仅是 notify 还不足以做到实时唤醒 T2 报警,上述代码无论执行多少次都是最后输出报警信息,想想这是为什么?

 1.3 wait-notify 完整版

  1. public class T_Alert03 {
  2. public static void main(String[] args) throws InterruptedException {
  3. Container container = new Container();
  4. final Object lock = new Object();
  5. new Thread(() -> {
  6. synchronized (lock) {
  7. if (container.sise() != 5) {
  8. try {
  9. lock.wait();
  10. } catch (InterruptedException e) {
  11. }
  12. }
  13. System.out.println("alert 5!");
  14. lock.notify();
  15. }
  16. }, "T2").start();
  17. TimeUnit.SECONDS.sleep(1);
  18. new Thread(() -> {
  19. synchronized (lock) {
  20. for (int i = 0; i < 10; i++) {
  21. container.add(new Object());
  22. System.out.println("已添加:" + container.sise());
  23. if (container.sise() == 5) {
  24. lock.notify();
  25. try {
  26. lock.wait();
  27. } catch (InterruptedException e) {
  28. }
  29. }
  30. }
  31. }
  32. }, "T1").start();
  33. }
  34. }

程序解析:该版本的 wait-notify 可以满足实际题目要求,达到实时触发警报,在 T1 notify 之后立刻调用 wait 进入状态;另一边T2在被唤醒并输出报警信息后,也需要再次调用 notify 唤醒 其他等待线程继续执行任务。

二、notify 和 notifyAll

如果有多个线程等待同一个对象锁,那么 notify 方法会随机唤醒一个线程,它无法做到精准唤醒。notifyAll 是唤醒全部等待线程,但需要明确是是,由于wait-notify 的操作模式是基于锁对象的,所以即便是 notifyAll 也是非公平竞争锁资源,即哪个线程抢到锁就去执行同步代码。

三、wait-notify 的原理

我们声明了一个 Object 对象,直接调用 wait 方法会怎样呢?

  1. public class T_Wait_Notify {
  2. public static void main(String[] args) throws InterruptedException {
  3. final Object lock = new Object();
  4. lock.wait();
  5. }
  6. }

监视器状态异常。这是因为 wait-notify 必须基于“锁对象”,而这个锁对象可不是普通的一个什么对象都可以。在了解了 synchronized 关键字的实现原理后,我们知道,JVM 为每个对象都关联了一个 monitor 对象,进入同步代码块和结束同步代码块就对应着 monitor enter 和 monitor exit 两条指令,也就是说,如果不使用 synchronized,就不存在所谓的 wait 和 notify。所以正确的写法一定是:

  1. synchronized (lock) {
  2. lock.wait();
  3. }

在 wait 方法的 Java doc 中这样说明,wait 方法会令当前线程将自己放入锁对象的 wait set 中,并且放弃此对象上所有同步的同步声明。

当唤醒时,当前线程必须持有该对象的监视器,才能继续执行。

总之,wait 和 notify 是和 对象的 monitor 紧密相关的,而 monitor 又是 synchronized 重量级锁模式的实现原理,所以理解wait 和 notify的 同时 也需要深入理解 synchronized 关键字。

扩展:闭锁实现的监控报警

闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。

闭锁相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,不允许任何线程通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。

下面的程序是通过 CountDownLatch 闭锁来实现的一个监控容器的版本,虽然可以达到要求,但很遗憾,程序中必须通过 sleep 方法让线程跑的“没那么快”,否则,去掉 sleep 的话依然会出现 while-true 的执行结果,即警报没那么实时了:

  1. public class T_Alert04CountDownLatch {
  2. public static void main(String[] args) {
  3. Container container = new Container();
  4. CountDownLatch latch = new CountDownLatch(1);
  5. new Thread(() -> {
  6. if (container.sise() != 5) {
  7. try {
  8. latch.await();
  9. } catch (InterruptedException e) {
  10. }
  11. }
  12. System.out.println("alert 5!");
  13. }, "T2").start();
  14. new Thread(() -> {
  15. for (int i = 0; i < 10; i++) {
  16. container.add(new Object());
  17. System.out.println("已添加" + container.sise());
  18. if (container.sise() == 5) {
  19. latch.countDown();
  20. }
  21. try {
  22. TimeUnit.SECONDS.sleep(1);
  23. } catch (InterruptedException e) {
  24. }
  25. }
  26. }, "T1").start();
  27. }
  28. }

总结

使用 wait-notify 切换线程状态是一种细粒度操作,开发者需要非常了解他们的执行逻辑以及线程的生命周期

wait 和 notify使用时必须将对象锁定,否则无法使用。

线程在使用对象的wait方法后会进入等待状态,notify() 和 notifyAll() 可以唤醒其他线程,注意 notify 是随机唤醒一个线程

wait-notify的操作是相对复杂的,虽然强大,但是在处理复杂的业务逻辑中书写较麻烦,相当于多线程中的汇编语言。

使用CountDownLatch可以有效的替代wait和notify的使用场景,而且不受锁的限制,书写简便。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/149166
推荐阅读
相关标签
  

闽ICP备14008679号