V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ZZMine
V2EX  ›  Java

问一个线程通信的问题

  •  1
     
  •   ZZMine · 309 天前 · 1675 次点击
    这是一个创建于 309 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

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

    这个例子中会出现问题:

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

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

    3. 用 while 就不一样了,用 while ,虽然有多个消费者线程重新竞争锁,并且有一个竞争成功,但是它在判断队列为空之后,又会因为 await/wait 进入条件队列
    gaifanking
        8
    gaifanking  
       309 天前
    就是解决伪唤醒问题,一方面操心系统都有几率出 bug 唤醒你,另一方面从业务开发来讲我们经常使用 notifyAll 而不是 notify ,这时多个排队的线程都会被唤醒的,但只能有一个去跑。
    hapeman
        9
    hapeman  
       308 天前   ❤️ 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
       305 天前
    @hapeman 好的,感谢大佬!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2845 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 13:32 · PVG 21:32 · LAX 05:32 · JFK 08:32
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.