[英]Visibility of a read after synchronized block
JMM 是否保证在synchronized
块之后对在另一个线程中读取的变量进行synchronized
写入的可见性? 这就是我的意思:
public class SynchronizedWriteRead {
private int a;
private int b;
public void writer() {
synchronized (this) {
a = 5;
b = 7;
}
}
public void reader() {
synchronized (this) {
int r1 = a; /* 5 */
}
int r2 = b; /* ? */
}
}
JMM 保证监视器上的解锁发生在该监视器上的每个后续锁定之前。 但我不确定它是否仅与synchronized
块体有关。
最近我在 Java 中遇到了 Aleksey Shipilëv 的这篇文章 - 安全发布和安全初始化。 它说:
请注意,在Unsafe DCL存储中进行
synchronized
是如何无济于事的,这与外行人的看法相反,它以某种方式神奇地“刷新了缓存”或诸如此类的东西。 在读取受保护的 state 时,如果没有配对锁,则无法保证在锁保护写入之前看到写入。
所以这就是我问自己这个问题的原因。 我在JLS中找不到答案。
让我们换一种说法。 有时你会捎带一个volatile
的发生之前的保证,如下所示:
public class VolatileHappensBefore {
private int a; /* specifically non-volatile */
private volatile int b;
public void writer() {
a = 5;
b = 7;
}
public void reader() {
int r1 = b; /* 7 */
int r2 = a; /* 5 */
}
}
因为同一线程中的顺序操作由happens-before 支持,并且happens-before 本身是可传递的,所以您可以保证看到这两种写入。
我可以以相同的方式使用synchronized
发生之前的保证吗? 甚至可能像这样(我已经放置了sync
变量以禁止编译器/JVM删除其他空的synchronized
块):
public void writer() {
a = 5;
b = 7;
synchronized (this) {
sync = 1;
}
}
public void reader() {
synchronized (this) {
int r = sync;
}
int r1 = a; /* ? */
int r2 = b; /* ? */
}
你现在已经得到了答案,公平地说,这里的每个人都是正确的,我只想添加一个重要的规则。 它被称为“在一致性之前发生”,它是这样的:
因此,虽然公认的答案确实是正确的,但应该提到,为了在(3)
和(4)
之间创建发生前的边缘, (4)
必须观察(3)
所做的写入。
在您的示例中:
public void reader() {
synchronized (this) {
int r = sync;
}
int r1 = a; /* ? */
int r2 = b; /* ? */
}
这意味着int r = sync;
不正确,您需要断言您实际上看到sync
为1
(您已观察到写入)。 例如,这将创建所需的边缘:
if (sync == 1) {
// guaranteed that r1=5 and r2=7
int r1 = a;
int r2 = b;
}
JMM 是否保证在
synchronized
块之后对在另一个线程中读取的变量进行synchronized
写入的可见性?
我可以以相同的方式使用同步发生之前的保证吗?
是的,是的。
synchronized
和volatile
提供了相同的可见性保证。
事实上,一个volatile
变量的行为就好像这个变量的每次读取和写入都发生在它自己的小synchronized
块中。
在JLS条款中:
非正式地,如果没有发生之前的命令以防止读取,则允许读取r查看写入w的结果。
volatile
和synchronized
是建立前发生关系的一些方法:
- 监视器上的解锁发生在该监视器上的每个后续锁定之前。
- 对 volatile 字段(第 8.3.1.4 节)的写入发生在对该字段的每次后续读取之前。
- ...
Java 中安全发布和安全初始化的引用描述了以下情况:
synchronized
块中初始化synchronized
块的情况下读取。在这种情况下,阅读器线程可能会在初始化过程中看到 object。
这里需要注意的重要一点是, happens-before是一个传递关系。 因此,如果A
发生在B
和B
发生在C
之前,我们可以安全地得出A
发生在C
之前的结论。
现在让我们看看有问题的代码(为了清楚起见,我添加了注释):
public void writer() {
a = 5; //1
b = 7; //2
synchronized (this) {
sync = 1;
} //3
}
public void reader() {
synchronized (this) { //4
int r = sync;
}
int r1 = a; //5
int r2 = b; //6
}
我们知道1
发生在2
之前和2
发生在3
之前,因为它们是由同一个线程执行的:
如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)。
我们也知道3
发生在4
之前:
监视器 m 上的解锁操作与 m 上的所有后续锁定操作同步(其中“后续”根据同步顺序定义)。
如果动作 x 与后续动作 y 同步,那么我们也有 hb(x, y)。
最后,我们知道4
发生在5
之前和5
发生在6
之前,因为它们在同一个线程中执行。
如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)。
所以我们最终得到了从 1 到 6 的一系列发生之前的关系。因此, 1
发生在6
之前。
JMM 是否保证在同步块之后对在另一个线程中读取的变量进行同步写入的可见性?
是的,由于其他答案中给出的原因。
但是有一个问题!
让我们假设reader
调用跟在writer
调用之后,因此在writer
退出其同步块和 reader 进入其块之间之前发生了一个事件......并传递它退出块并读取b
。
问题是当reader
退出其块时,不再保证b
上的互斥。 所以假设另一个线程立即获取互斥锁并修改b
。 由于没有 HB 链将写入b
与 read in reader
连接起来,因此无法保证reader
代码实际看到的更新...或writer
之前写入的值。
简而言之,在推理并发算法时,需要考虑的不仅仅是HB 关系。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.