问一个线程通信的问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ZZMine
V2EX    Java

问一个线程通信的问题

  •  1
     
  •   ZZMine 2024-01-19 14:30:25 +08:00 2232 次点击
    这是一个创建于 685 天前的主题,其中的信息可能已经有所发展或是发生改变。

    疑惑:getTask()方法中判断 queue.isEmpty()的时候为什么一定要用 while 而不是 if 呢?

    代码如下:

    class TaskQueue { Queue<String> queue = new LinkedList<>(); public synchronized void addTask(String s) { this.queue.add(s) this.notifyAll(); } public synchronized String getTask() throws InterruptedException { while (queue.isEmpty()) { this.wait(); } return queue.remove(); } } 

    教程中的解释如下,但是自己也没理解明白。 “if 的写法实际上是错误的,因为线程被唤醒时,需要再次获取 this 锁。多个线程被唤醒后,只有一个线程能获取 this 锁,此刻,该线程执行 queue.remove()可以获取到队列的元素,然而,剩下的线程如果获取 this 锁后执行 queue.remove(),此刻队列可能已经没有任何元素了,所以,要始终在 while 循环中 wait(),并且每次被唤醒后拿到 this 锁就必须再次判断”

    新手学习 java 线程通信,还请大佬们指导指导。

    10 条回复    2024-01-23 10:54:47 +08:00
    xx6412223
        1
    xx6412223  
       2024-01-19 14:41:33 +08:00   1
    1. object wait 会释放锁,也就是可能有多个线程在 this.wait()等待唤醒。而 while 会某个线程被唤醒后会再次检查 queue.isEmpty(),而 if 会直接向下运行。
    2. 更易读的方式是使用 ConcurrentLinkedQueue
    Znemo
        2
    Znemo  
       2024-01-19 14:41:59 +08:00   1
    两个线程调用 `getTask` 的场景,如果使用 if ,其中一个线程会消费掉队列中的数据,接着第二个线程会在 wait 处被唤醒,继续向下执行,错误地调用 `queue.remove()`
    vagusss
        3
    vagusss  
       2024-01-19 14:42:57 +08:00   1
    用 if,线程被唤起后, 不会再次判断条件
    用 while,线程被唤起后, 会再次判断条件
    vagusss
        4
    vagusss  
       2024-01-19 14:45:28 +08:00
    @vagusss 多线程环境下, 如果线程苏醒后不再次判断 queue.isEmpty(), 那么直接 remove 是可能出问题的
    falsemask
        5
    falsemask  
       2024-01-19 14:47:37 +08:00   1
    可能存在虚假唤醒,可以参考一下这个知乎回答 https://www.zhihu.com/question/271521213
    ZZMine
        6
    ZZMine  
    OP
       2024-01-19 15:10:35 +08:00
    好的明白了,感谢大家~ 主要是 this.wait()被唤醒后还是继续执行的,而不是把方法再重新执行。
    9c04C5dO01Sw5DNL
        7
    9c04C5dO01Sw5DNL  
       2024-01-19 15:21:36 +08:00
    有两个原因,展开讲一下第一个原因。

    这个例子中有两个关于锁的队列,一个是 CLH 锁队列,即:获取和释放锁时的队列。还有一个是条件队列,即 notify/wait/signal/await 这种。

    在需要所但是没有获取到锁的时候,线程进入锁队列。当线程获取到锁又 wait/await 的时候,它会做两个动作
    1. 释放锁
    2. 线程转移到条件队列,不再在所队列中。

    这个例子中会出现问题:

    1. 在初始队列为空,假设当生产线程添加 1 个元素、notify 并退出同步代码块之后,那么会有多个消费者线程从条件队列转移到了锁队列中,并且有一个消费者线程获取到了锁。

    2. 如果不用 while ,而是用 if ,那么当获取到锁的消费者线程消费完,队列为空,此时消费者释放锁,这会导致其他消费者线程重新竞争锁。因为它们现在是在锁队列中,而不是在条件队列中。不幸的是,现在队列中唯一的元素已经被消费了。

    3. 用 while 就不一样了,用 while ,虽然有多个消费者线程重新竞争锁,并且有一个竞争成功,但是它在判断队列为空之后,又会因为 await/wait 进入条件队列
    gaifanking
        8
    gaifanking  
       2024-01-19 19:29:11 +08:00
    就是解决伪唤醒问题,一方面操心系统都有几率出 bug 唤醒你,另一方面从业务开发来讲我们经常使用 notifyAll 而不是 notify ,这时多个排队的线程都会被唤醒的,但只能有一个去跑。
    hapeman
        9
    hapeman  
       2024-01-20 12:31:48 +08:00   1
    两个消费者线程先后执行 getTask(),此时队列为空,两个消费者线程执行 wait()进行休眠
    之后 一个生产者线程执行 addTask()向消费队列添加了一个任务(队列长度为 1 )并通过 notifyAll()唤醒了所有消费者线程,此时如果是 if 判断,消费者线程 1 获取到锁并执行了 remove()后释放锁,,由于是 if 语句 消费者线程 2 此时会等待线程 1 释放锁后继续执行下面的语句,而此时队列已经为空了去调用 remove()方法会抛出异常;而改用 while 语句消费者线程 2 获取到锁之后仍会进去 while 循环判断队列是否为空,调用 wait()方法

    可以看一下 https://cloud.tencent.com/developer/article/2281627 中生产者消费者模块提到了这个问题-虚假唤醒
    ZZMine
        10
    ZZMine  
    OP
       2024-01-23 10:54:47 +08:00
    @hapeman 好的,感谢大佬!
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3225 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 77ms UTC 11:26 PVG 19:26 LAX 03:26 JFK 06:26
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86