问一个线程通信的问题

113 天前
 ZZMine

疑惑: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 线程通信,还请大佬们指导指导。

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

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

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

这个例子中会出现问题:

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

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

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

可以看一下 https://cloud.tencent.com/developer/article/2281627 中生产者消费者模块提到了这个问题-虚假唤醒
ZZMine
109 天前
@hapeman 好的,感谢大佬!

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1010005

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX