我正在看一个我找到的最简单的例子,并开始推理SO
(同步顺序),或者更准确地说,缺乏同步顺序。
int a, b; // two shared variables
Thread-X:
void threadX() {
synchronized(this) {
a = 1;
}
synchronized(this) {
b = 1;
}
}
以及一个读取器线程Thread-Y
:
void threadY() {
int r1 = b;
int r2 = a;
}
为了简单起见,我们假设Thread-Y
完全按照以下顺序进行读取:它将肯定首先读取x1M3 N1 x,然后读取x1M4 N1 x(与写入相反)。
阅读线程是允许看到[1, 0]
的(就像b=1
发生在 * a=1
之前一样).我想我也明白为什么:因为这两个动作之间没有同步顺序,因此不存在 happens-before,并且根据JLS
,这是一个 * 数据竞争 *:
当程序包含两个不按happens-before关系排序的冲突访问时,就称为包含数据争用。
因此,阅读a
和b
是两个活泼的读取,所以看到b=1
和a=0
是允许的并且是可能的。
现在,这反过来又允许JVM在编写器中进行锁粗化,因此它变成:
void threadX() {
synchronized(this) {
a = 1;
b = 1;
}
}
我的问题是,如果读者最初是这样写的:
void threadY() {
synchronized(this) {
int r1 = b;
}
synchronized(this) {
int r2 = a;
}
}
锁粗化仍然被允许吗?我 * 想 * 我知道答案,但我也想听到一个有教养的解释。
3条答案
按热度按时间mnemlml81#
是的,允许。
这里有一个简单的解释。
请记住,
synchronized
块:synchronized
块,而另一个线程正在持有同一个锁synchronized
块时,它立即看到在先前执行的synchronized
块中所做的一切换句话说,
synchronized
块总是以全局顺序原子执行。不同的执行可以在synchronized
块的交叉方式上有所不同,但情况总是这样:threadX()
中的第一个synchronized
块总是在第二个块之前执行1.与
threadY()
中的synchronized
块相同有6种可能的交织:
当您合并
threadY()
中的synchronized
块时:这样,您实际上只将
threadY()
中的synchronized
块彼此相邻的情况保留为允许的情况:即情况A、C和D。由于在此优化之后没有出现新的可能执行,因此此优化是法律的的。
对于更严格和详细的解释,我建议:
ruarlubt2#
锁粗化(和重排序)是允许的,因为同步读取器要么在粗化锁之前排序,要么在粗化锁之后排序。它们永远无法看到在粗化锁被持有时发生了什么,因此无法观察到锁定代码中的任何重排序。
如需详细信息,请参阅:https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#myth-barriers-are-sane
顺便说一句,这个问题问得好。一段时间前,我也在纠结这个特殊的例子:)
quhf5bfb3#
如果JVM能够证明后续的
synchronized
块将使用相同的对象,那么锁粗化总是可能的。也可能得到优化
但是如果两个方法都是同一个类的示例方法,换句话说,它们的
this
引用同一个对象,它们的执行就不可能重叠,因此,即使读和/或写被重新排序,结果[1, 0]
也是不可能的。只要执行环境能够确保结果
[1, 0]
永远不会发生,即使是锁消除也是允许的。一个众所周知的例子是对象被证明永远不会被另一个线程看到的场景。