为什么我们必须使用“while”来检查竞争条件而不是“if”

6
我在“Java编程思想”一书中读到了以下代码。
synchronized(obj)
    {
        while (condition_not_matched)
        {
            obj.wait();
        }
        //continue
        dosomething();

    }

我认为:
使用 "if" 是可以的,因为 "wait" 意味着它必须获取 obj 的锁监视器,并且只有一个线程可以在此处执行。
(1) 为什么这里使用 "while (condition)" 而不是 "if"?
(2) 执行 "obj.wait()" 时会发生什么?当前线程是否释放了 "obj" 的锁?
(3) 当另一个线程执行 "obj.notify()" 时,上一个线程会发生什么?(它是否重新获取了 "obj" 的锁?如果是,则必须条件不匹配,因此 "if" 就足够了。)
我错了吗?

4
阅读wait()和notify()的Javadoc(还应优先选择notifyAll()),就可以回答任何问题。 - JB Nizet
1
我不会说notifyAll“应该被优先考虑”。有些情况下它是恰当的,而有些情况下则不是。 - Wyzard
https://dev59.com/9lwY5IYBdhLWcg3wTWMu - ZhongYu
@Wyzard,应该优先选择使用notifyAll()的情况是当你向不完全理解自己在做什么的新手提供小块建议时。如果你在可以使用notify()的情况下使用notifyAll(),唯一的后果是程序的性能不如可能达到的那样好。如果你在应该使用notifyAll()的地方使用notify(),那么后果可能比这更糟糕。 - Solomon Slow
3个回答

12
使用if语句而不是在循环中重复检查是一个错误。有几个原因可以使用循环。
其中一个原因是“虚假唤醒”,这意味着等待方法可能在没有通知线程的情况下返回:不能根据线程退出等待方法来推断它一定已经被通知了。虽然这种情况可能不常发生,但必须处理这种可能性。
但主要原因是这样的:当线程等待时,它释放了锁。当它接收到通知时,它没有锁,并且必须重新获取锁才能退出等待方法。仅仅因为线程被通知并不意味着它是下一个获得锁的线程。如果线程基于在没有拥有锁的情况下发生的某些事件来决定下一步该做什么,那么在通知发生和线程获得锁之间,多个线程可能有机会对同一个共享对象进行操作,而共享对象的状态可能与线程认为的不同。使用while循环允许线程在持有锁的情况下再次检查等待的条件,确认条件在继续之前仍然有效。

虽然有很多解释可用,但您所解释的水平确实有助于理解这个难题。为此点赞。 - Parveen Verma

2
需要使用循环的原因在于wait方法的Javadoc中有所解释:

线程也可以在没有被通知、中断或超时的情况下唤醒,这种情况称为虚假唤醒。虽然这种情况在实践中很少发生,但应用程序必须通过测试应该导致线程被唤醒的条件并在条件不满足时继续等待来防范它。

为了防止这种情况,当wait()调用返回后,您必须再次检查条件,如果条件为假,则必须返回并再次调用wait()而不是继续执行。使用while循环可以实现这一点。
当调用wait()时,等待时对象的锁会自动释放,然后在方法返回之前重新获取。这意味着当另一个线程在对象上调用notify()时,等待线程不能立即恢复运行,因为通知线程仍然持有对象的锁,等待线程必须等待其释放。这也意味着如果有多个等待线程,并且您调用notifyAll(),则等待线程不能同时恢复:其中一个线程将获得锁并从wait()返回,当它释放锁时,然后另一个线程可以获得它并从wait()返回,以此类推。
在涉及多个等待线程的某些情况下,等待线程可能会唤醒,发现条件为真,并执行一些导致将条件更改回false的操作——同时保持锁定。然后,当它再次释放锁(例如通过再次调用wait())时,下一个线程将唤醒并发现条件为false。在这种情况下,它不是虚假唤醒;条件确实变为true,但然后在线程有机会检查它之前又变为false了。
例如:生产者线程向队列添加多个项目,并调用notifyAll()来唤醒消费者线程。每个消费者线程从队列中取出一个项目,然后在处理该项目时释放锁定。但是,如果消费者线程比添加到队列中的项目还要多,则其中一些线程将唤醒,只发现队列为空,因此它们必须返回等待状态。
使用while循环检查条件可以处理这种情况,并处理虚假唤醒。

1

if语句通过运行一次来检查表达式是否为真或假,然后仅在其为真时才运行语句内的代码。

while条件会持续执行while语句中的代码,直到表达式为真。此外,当您不知道需要循环多少次时,使用while循环更加合适。

obj.wait() - 导致当前线程等待,直到另一个线程调用相应对象的notify()方法或nofityAll()方法。如果将超时作为参数传递,则线程将等待一定时间后再继续执行。

obj.notify()会唤醒在相应对象监视器上等待的单个线程。只有在当前线程放弃对对象的锁定后,唤醒的线程才会继续进行。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接