• java多线程并发环境下为什么使用while而不用if


    目录

    前言

    一 . 使用if所引发的问题

    1.1 虚假唤醒

     1.2 虚假唤醒代码示例

     1.3 解决虚假唤醒

    二 . 为什么用while就能解决问题

    结论


    前言

    再开始本文之前 , 先给大家看一张图 , 这是Object.wait()的源码介绍 , 翻译过来内容如下

            使当前线程等待另一个线程调用此对象的notify()方法或notifyAll()方法.  换句话说 , 这个方法就像他只是调用 wait(0) 一样 .

            当前线程必须拥有此对象的监视器。线程释放此监视器的所有权,并等待另一个线程通过调用notify方法或notifyAll方法通知等待此对象监视器的线程唤醒。然后,线程等待,直到它可以重新获得监视器的所有权并恢复执行。

            注意红框圈住部分 , 说的是 中断和虚假环境也是可能的 , 所以这种方法应该是重在循环内 ,  至于为什么说 请继续往下看. 

            

    一 . 使用if所引发的问题

    1.1 虚假唤醒

            虚假唤醒这种情况只会出现在多线程环境中,指的是在多线程环境下,多个线程等待在同一个条件上,等到条件满足时,所有等待的线程都被唤醒,

            当一定的条件触发时会唤醒很多在阻塞态的线程 , 其中一部分线程从条件变量中苏醒过来时,发现等待的条件并没有满足 ,  这便是虚假唤醒

     1.2 虚假唤醒代码示例

    我们在使用线程时,进行条件判断时,往往会先考虑使用if进行判断,在线程进行等待时就会出现不确定的结果。先来看看两个线程下的操作。

    首先创建一个模拟业务类, 也是一个经典案例 "生产者&消费者" 

    1. public class MyService {
    2. private int num = 1;
    3. /**
    4. * 模拟上架货物 , 始终保证有货
    5. */
    6. @SneakyThrows
    7. public synchronized void plus() {
    8. // 当货物数量大于0 , 不缺货 , 不用补 , 使当前线程休眠
    9. if (num > 0) {
    10. System.out.println("num = " + num + " plus线程休眠");
    11. // wait() 使当线程停在此处, 直到notify() 或者 notifyAll()被唤醒后 , 接着向下执行
    12. this.wait();
    13. System.out.println("num = " + num + " plus线程被唤醒");
    14. }
    15. // num 自加(补货
    16. num++;
    17. // 补货完毕 , 唤醒等待当前对象的其他线程前来竞争锁
    18. this.notify();
    19. System.out.println("当前num == " + num);
    20. }
    21. /**
    22. * 模拟消费货物
    23. */
    24. @SneakyThrows
    25. public synchronized void sub() {
    26. // 当货物数量num <= 0 时说明没货了 , 线程休眠
    27. if (num <= 0) {
    28. System.out.println("num = " + num + " sub线程休眠");
    29. this.wait();
    30. System.out.println("num = " + num + " sub线程被唤醒");
    31. }
    32. // num 自减
    33. num--;
    34. // 唤醒等待当前对象的其他线程来竞争锁
    35. this.notify();
    36. System.out.println("当前num == " + num);
    37. }
    38. }

    然后分别创建补货/售货两个线程 , 用来模拟后续的动作

    1. // 补货线程
    2. public class PlusThread implements Runnable {
    3. private MyService myService;
    4. public PlusThread(MyService myService) {
    5. this.myService = myService;
    6. }
    7. @Override
    8. public void run() {
    9. for (int i = 0; i < 10; i++) {
    10. myService.plus();
    11. }
    12. }
    13. }
    14. // 售货线程
    15. public class SubThread implements Runnable {
    16. private MyService myService;
    17. public SubThread(MyService myService) {
    18. this.myService = myService;
    19. }
    20. @Override
    21. public void run() {
    22. for (int i = 0; i < 10; i++) {
    23. myService.sub();
    24. }
    25. }
    26. }

    接下来创建一个测试类 , 用来模拟多线程下的生产/消费 

    1. /**
    2. * @author hxk
    3. * @version test: ThreadTest01.java, v 0.1 2022-06-30 10:13 hxk Exp $
    4. */
    5. public class ThreadTest01 {
    6. @SneakyThrows
    7. public static void main(String[] args) {
    8. // new 一个业务类
    9. MyService myService = new MyService();
    10. // 补货线程
    11. PlusThread plusThread = new PlusThread(myService);
    12. // 售货线程
    13. SubThread subThread = new SubThread(myService);
    14. // 然后一个补货 , 一个售货
    15. Thread p = new Thread(plusThread);
    16. p.start();
    17. Thread s = new Thread(subThread);
    18. s.start();
    19. }
    20. }

            执行main方法,结果如下图 .

      这样执行得到的结果是正常的 , 暂时也看不出问题 , 可是当我们在此基础之上 , 将售货的线程再增加一个 , 问题就暴露出来了 , main方法中追加一下代码 , 再去执行看下效果

    1. Thread s = new Thread(subThread);
    2. s.start();

     连续执行几次后发现 , 其中几次执行结果出现了 负值 , 为了方便大家理解个中缘由 , 我给所有线程都加上名字 , 便于大家理解 .

     

     然后这里重新执行 几次 , 得到错误的日志入下 , 我将图中的几个关键点标记了起来 

     首先是关键点1  , 这一步是属于正常唤醒 , num = 0 了 , p0线程需要重新补货, 所以p0分别执行了num++ 和 , notify() 唤醒等待此对象的其他线程 

    然后就到了关键点2 , 这时候售货线程s1被唤醒 , 对num进行num-- , 同样的再执行notify() , 来唤醒等待该对象的其他线程 ,

    这时候最关键的关键点3 , 本应该被唤醒的是p0 , 结果却唤醒了s0 , 而s0被唤醒的位置至关重要 , 那就是wait()函数后面, 注意这时候 已经处在 If 条件内了 , 就不在进行判断了 , 而是继续向下执行下去了 ,  再一次 num--  , 这便出现了最开始看到的负数, 也就是虚假唤醒(线程从条件变量中苏醒过来时,发现等待的条件并没有满足) 

     1.3 解决虚假唤醒

    要想解决虚假唤醒这个问题 , 其实在wait()的源码备注中就有给出解决方案, 同时也有说明 中断和虚假环境也是可能的 , 所以这种方法应该是重在循环内 ,

     改良后代码 

    1. public class MyService {
    2. private int num = 1;
    3. /**
    4. * 模拟上架货物 , 始终保证有货
    5. */
    6. @SneakyThrows
    7. public synchronized void plus() {
    8. // 当货物数量大于0 , 不缺货 , 不用补 , 使当前线程休眠 , 改良后 此处判断变为了 while
    9. while (num > 0) {
    10. System.out.println("num = " + num + ", " + Thread.currentThread().getName() + "线程休眠");
    11. // wait() 使当线程停在此处, 直到notify() 或者 notifyAll()被唤醒后 , 接着向下执行
    12. this.wait();
    13. System.out.println("num = " + num + ", " + Thread.currentThread().getName() + "线程被唤醒");
    14. }
    15. // num 自加(补货
    16. num++;
    17. // 补货完毕 , 唤醒等待当前对象的其他线程前来竞争锁
    18. this.notify();
    19. System.out.println("当前num == " + num);
    20. }
    21. /**
    22. * 模拟消费货物
    23. */
    24. @SneakyThrows
    25. public synchronized void sub() {
    26. // 当货物数量num <= 0 时说明没货了 , 线程休眠 改良后 此处判断变为了 while
    27. while (num <= 0) {
    28. System.out.println("num = " + num + ", " + Thread.currentThread().getName() + "线程休眠");
    29. this.wait();
    30. System.out.println("num = " + num + ", " + Thread.currentThread().getName() + "线程被唤醒");
    31. }
    32. // num 自减
    33. num--;
    34. // 唤醒等待当前对象的其他线程来竞争锁
    35. this.notify();
    36. System.out.println("当前num == " + num);

    二 . 为什么用while就能解决问题

    首先我们先来回顾一下java 基础 里面  if 和while 的区别  ,

    结论

            一个被唤醒的线程就处于就绪状态了,就可以等待被cpu调度了,

            如果用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码.   

            而使用while虽然也会从wait之后的代码开始运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。

            所以必须用while来检查,这样可以保证每次被唤醒都会检查一次条件。

            

     

  • 相关阅读:
    详解容灾架构中的脑裂问题
    AWS 认证报名考试流程
    TDengine 常见问题汇总
    【Vue3.0移动端项目--旅游网】-- 城市页面搭建
    MySQL初学知识总篇
    PHP志愿者协会报名系统的设计与实现 毕业设计-附源码201524
    【网页设计】基于HTML在线商城购物项目设计与实现-----电脑商城6页带视频 带报告3000字
    设备树学习
    先进的人工智能促进更好的业务沟通
    002-JVM 常用命令
  • 原文地址:https://blog.csdn.net/qq_42543063/article/details/125526543