请考虑以下代码示例:
private Object lock = new Object();
private volatile boolean doWait = true;
public void conditionalWait() throws Exception {
synchronized (lock) {
if (doWait) {
lock.wait();
}
}
}
public void cancelWait() throws Exception {
doWait = false;
synchronized (lock) {
lock.notifyAll();
}
}
如果我正确理解Java Memory Model,则上面的代码不是线程安全的。它可能会很好地阻塞,因为编译器可能会决定重新排列代码,如下所示:
public void cancelWait() throws Exception {
synchronized (lock) {
lock.notifyAll();
}
doWait = false;
}
在这种情况下,线程T1可能会调用cancelWait()
方法,获取锁定,调用notifyAll()
并释放锁定。在此之后,并行线程T2可以调用conditionalWait()
并获取现在可用的锁。变量doWait
仍然具有值true,因此线程T2执行lock.wait()
并阻塞。
我的理解是否正确?如果没有,那么请提供Java规范的参考,这反映了上述情况。
是否有解决此问题的解决方案不需要将doWait
拉入同步块?
答案 0 :(得分:2)
您的代码已损坏,但不是因为重新排序或可见性问题。在没有足够的同步的情况下发生重新排序问题,这不是这种情况。在标记易失性或同步的方面,你已经尽了一切可能,让JVM知道在线程中看到正确的东西。
你的问题在于你做了几个错误的假设:
您认为等待直到收到通知才会返回(这可能不会经常发生,但可能会发生,这称为“#34;虚假唤醒"”)。
您假设另一个线程无法在通知发生的时间和等待线程重新获取监视器的时间之间插入。 (Object#wait释放监视器,在重新获取监视器时,线程需要重新检查当前状态,而不是基于可能过时的假设继续进行。)
您假设您可以预测通知将在等待后发生(在此情况下,由于您没有发布完整信息,因此无法说明这是否属实)工作示例,但一般来说这不是你想要的假设。)
有许多玩具示例(考虑偶数分配)可以逃脱这个因为它们仅限于2个线程,导致虚假唤醒的竞争条件并不经常发生在PC JVM上,并且程序强制两个线程以锁定步骤操作,因此事件发生的顺序是可预测的。但这些对现实世界来说并不是现实的假设。
对这些错误假设的解决方法是使用条件变量等待循环,以决定您何时等待(参见this Oracle tutorial):
private final Object lock = new Object(); // final to emphasize this shouldn't change
private volatile boolean doWait = true;
public void conditionalWait() throws InterruptedException {
synchronized (lock) {
while (doWait) {
lock.wait();
}
}
}
public void cancelWait() {
doWait = false;
synchronized (lock) {
lock.notifyAll();
}
}
(我缩小了抛出的异常,notifyAll抛出的唯一东西是IllegalMonitorStateException,它是未经检查的,只要您使用正确的锁定就不会发生,它只会被抛出程序员错误的结果。 Object#wait抛出InterruptedException以及IllegalMonitorStateException,它可以在这里抛出它。)
将doWait变量的引用移动到synchronized块中也是如此,如果在保持锁定的同时对它进行了所有引用,那么您就不需要使它变得易变。但这并不是必需的。
答案 1 :(得分:2)
你问的问题实际上是
可以在易失性商店上方重新订购显示器吗?
不,你的转变不可能发生。看看http://gee.cs.oswego.edu/dl/jmm/cookbook.html顶部链接的网格。
First Operation: Volatile Store
Second Operation: Monitor Enter
Result: No
因此编译器无法按照您的建议重新排序。
答案 2 :(得分:-1)