我最近阅读了http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html,它清楚地描述了许多Java内存模型的内在函数。一个特别的摘录引起了我的注意:
The rule for a monitorexit (i.e., releasing synchronization) is that
actions before the monitorexit must be performed before the monitor is released.
对我来说很明显,但是在阅读http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html并且发生之前 - 在定义之前,我发现关于监视器解锁的所有内容都是当一个线程解锁监视器发生 - 之前另一个线程再次锁定它(这也很有意义)。有人可以解释JLS如何解释在解锁操作之前必须发生同步块内所有操作的明显情况吗?
进一步的评论:
基于几个回复,我想写下你们所说的话的进一步评论:
我引用来源的几个“真理”:
a = new A()
如果new A()
涉及一百个操作,然后将堆上的地址分配给a
,编译器可以简单地重新排序那些以将堆地址分配给a
,然后按照惯例初始化(双重检查锁定的问题)
synchronized{
a = 5;
}
print a;
可以更改为
synchronized{
a = 5;
print a;
}
我们使用print语句重新排序monitorexit
(根据JLS也有效)
现在,我提到了一个简单的案例:
x = 1;
y = 2;
c = x + y;
print c;
我认为没有理由阻止编译器先分配x或先分配x。完全没有什么能阻止它,因为不管x是先分配还是y分配,最终输出都不变。所以重新排序是完全可能的。
基于打印语句被“拉入”同步块的示例,让我们尝试将其反转,即启动
synchronized{
a = 5;
print a;
}
我可以期待编译器这样做:
synchronized{
a = 5;
}
print a;
在单线程世界中似乎完全合理, YET 这绝对是无效的,并且针对JLS(根据引用的来源)。现在为什么会这样,如果我在JLS中找不到任何关于此的内容?显然,关于“程序顺序”的动机现在无关紧要,因为编译器可以进行重新排序,例如将语句“拉入”同步块。
答案 0 :(得分:2)
这不仅仅是synchronized
块内执行的所有操作,它还指的是monitorexit
之前该线程的所有操作。
有人可以解释JLS如何解释所有的明显条件 同步块内的动作必须在 - 之前发生 解锁操作?
对于特定线程(并且只有一个线程),无论synchronized
如何,所有操作都会维护程序顺序,因此看起来好像所有读取和写入都按顺序发生(我们不需要在发生之前进行排序)单线程案例)。
before-before关系考虑了多个线程,即在monitorexit之前的一个线程中发生的所有操作在连续monitorenter
之后对所有线程都可见。
编辑以解决您的更新问题。
编译器必须遵循特定规则才能重新排序。在这种情况下的具体问题在可以重新排序网格中进行了演示here
特别有用的条目是
此处的值为否,这意味着编译器无法重新排序两个操作,其中第一个是正常加载,第二个是monitorexit
,因此在您的情况下,此重新排序将违反JLS。
有一个称为roach-motel排序的规则,即读/写可以重新排序到同步块中,但不能从中排除。
答案 1 :(得分:1)
也许你错过了this(§17.4.5):
如果x和y是同一个线程的动作,并且x在程序顺序中位于y之前,那么hb(x,y)。
结合您已经知道的发生在之前的事情,应该清楚这意味着解锁操作之前的所有操作都将对其他线程可见。
关于你对这个问题的补充,如果你这样写:
synchronized {
a = 5;
b = 3;
}
并且编译器发出:
synchronized{
a = 5;
}
b = 3;
然后违反了上面引用的规定:现在b = 3
在锁定释放之前不会发生。这就是为什么它是非法的。 (请注意,使用print a
的示例并不具有指导性,因为它只涉及读取+副作用,而这些副作用不易用简单变量描述。)