happen-before 属性 与可见性和排序有何关系?
How does the happen-before property relate to visibility and ordering?
我想弄清楚 属性 之前发生的事情到底是什么意思。
我看过对 happens-before 属性 的解释说,如果全局变量(不是易变的或包含在同步块中)的更新在某些线程之前被更改,则它们对其他线程可见在同步块中易变或改变的其他变量。这是正确的吗?如果是这样,java 文档中的哪个地方是这样说的?
我的理解是 happens-before 属性 定义了共享字段和代码执行之间的关系,例如:
- 监视器的解锁发生在同一监视器的每个后续锁定之前。
- 对可变字段的写入发生在同一字段的每次后续读取之前。
- 线程启动调用发生在启动线程中的任何操作之前。
- 线程中的所有操作发生在任何其他线程成功之前 return来自该线程上的连接。
例如:
class Shared {
private int y = 0;
private volatile int x = 0;
public void setOne() {
y = 1;
x = 1;
}
public int getY() {
return y;
}
}
对于上面的代码,给定 2 个线程:
Shared shared = new Shared();
new Thread(() -> shared.setOne()).start();
new Thread(() -> shared.getY()).start();
编辑
假设我们可以保证第一个线程已经启动,这里getY() return 0还是1?
我还看到一些例子说这种行为只发生在读取线程中的易失性字段之后。因此,在这种情况下,如果一个线程读取易失性字段的值(假设线程 B),则线程 A 中该易失性字段之前写入的所有字段都可用于线程 B。据此,如果我修改 getY() 方法上面的共享对象为:
public int getXPlusY() {
int local = x;
return local + y;
}
是否是此操作使 y 对其他线程可见?
我们先看你的第二个例子。
class Shared {
private int y = 0;
private volatile int x = 0;
public void setOne() {
y = 1; //(1)
x = 1; //(2)
}
public int getXPlusY() {
int local = x; //(3)
return local + y; //(4)
}
}
由于程序顺序,我们知道 (1) 和 (2) 之间存在 happens-before 关系:
If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).
由于 x
是易变的,我们知道 (2) 和 (3)
之间存在 happens-before 关系
A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.
由于程序顺序,(3)和(4)之间又存在happens-before关系:
If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).
因此,我们有一个 happens-before 链 (1) → (2), (2) → (3), (3) → (4)
并且由于 happens-before 是一个传递关系(如果 A 发生在 B 之前并且 B 发生在 C 之前,那么 A 发生在 C 之前)这意味着 (1) 与 (4).
有 happens-before 关系
现在让我们看第一个例子:
class Shared {
private int y = 0;
private volatile int x = 0;
public void setOne() {
y = 1; //(1)
x = 1; //(2)
}
public int getY() {
return y; //(3)
}
}
(1) 和 (2) 之间又存在 happens-before 关系,仅此而已。因为 x
没有被第二个线程读取,所以我们在 (2) 和 (3) 之间没有 happens-before。因此我们在 (1) 和 (3).
之间 没有 happens-before 关系
我将重命名 类 以使其清楚:
class First {
private int y = 0;
private volatile int x = 0;
public void setOne() {
y = 1;
x = 1;
}
public int getY() {
return y;
}
}
而你的前提条件是:
Assuming we can guarantee that the first thread has started
getY
将 return 1
的保证为零。仅当您遵守 JLS 的 happens-before 规则时,才能保证对共享字段的更新对某些阅读线程可见,此处强制执行 none。因此从 getY
得到 0
是完全有效的。
你的第二个例子:
class Second {
private int y = 0;
private volatile int x = 0;
public void setOne() {
y = 1;
x = 1;
}
public int getXPlusY() {
int local = x;
return local + y;
}
}
同样前提条件是:
Assuming we can guarantee that the first thread has started
不保证return2
。对于输出 2
的方法,读取线程必须观察(查看)volatile 写入。也就是说,它必须遵循以下原则:
public int getXPlusY() {
int local = x;
if(local == 1){ // you need to see the write that was done in the other thread
return local + y;
}
return local + y;
}
如果您输入 if 语句 f(local == 1)
,您会观察到在另一个线程 (x = 1
) 中完成的易失性写入,因此,您可以保证之前完成的所有操作那写也是可见的。这意味着在这个时间点,肯定 y = 1
,所以你的方法将 return 2
.
如果没有这个 if 语句,你的阅读 int local = x
是一个活泼的阅读,没有什么可以阻止这个线程在两者之间做 int local = x
:
y = 1;
x = 1;
事实证明这也是可以证明的,这是一个 jcstress
证明另一个答案(至少 1/2)错误的测试:
//
@JCStressTest
@State
@Outcome(id = "0", expect = Expect.FORBIDDEN, desc = "can not happen")
@Outcome(id = "1", expect = Expect.FORBIDDEN, desc = "can not happen")
@Outcome(id = "2", expect = Expect.ACCEPTABLE, desc = "reader thread was done before writer did anything")
@Outcome(id = "3", expect = Expect.ACCEPTABLE_INTERESTING, desc = "racy read!!!")
@Outcome(id = "4", expect = Expect.ACCEPTABLE, desc = "reader thread sees everything that writer did")
public class VolatileRace {
// change defaults so that "readerThread" does not output a false positive.
// it can happen when it runs it's entire method body before "writerThread"
// does anything at all.
private int y = 1;
private volatile int x = 1;
@Actor
public void writerThread() {
y = 2;
x = 2;
}
@Actor
public void readerThread(I_Result result) {
int local = x;
result.r1 = local + y;
}
}
这个测试的重点是,它有这样的输出:
0 0 FORBIDDEN can not happen
1 0 FORBIDDEN can not happen
2 4,616,926 ACCEPTABLE reader thread was done before writer did anything
3 6,694 ACCEPTABLE_INTERESTING racy read!!!
4 3,107,831 ACCEPTABLE reader thread sees everything that writer did
即使看不懂,也说明读线程看到了3
的结果,也就是说读到了x = 1
和y = 2
。
我想弄清楚 属性 之前发生的事情到底是什么意思。
我看过对 happens-before 属性 的解释说,如果全局变量(不是易变的或包含在同步块中)的更新在某些线程之前被更改,则它们对其他线程可见在同步块中易变或改变的其他变量。这是正确的吗?如果是这样,java 文档中的哪个地方是这样说的?
我的理解是 happens-before 属性 定义了共享字段和代码执行之间的关系,例如:
- 监视器的解锁发生在同一监视器的每个后续锁定之前。
- 对可变字段的写入发生在同一字段的每次后续读取之前。
- 线程启动调用发生在启动线程中的任何操作之前。
- 线程中的所有操作发生在任何其他线程成功之前 return来自该线程上的连接。
例如:
class Shared {
private int y = 0;
private volatile int x = 0;
public void setOne() {
y = 1;
x = 1;
}
public int getY() {
return y;
}
}
对于上面的代码,给定 2 个线程:
Shared shared = new Shared();
new Thread(() -> shared.setOne()).start();
new Thread(() -> shared.getY()).start();
编辑 假设我们可以保证第一个线程已经启动,这里getY() return 0还是1?
我还看到一些例子说这种行为只发生在读取线程中的易失性字段之后。因此,在这种情况下,如果一个线程读取易失性字段的值(假设线程 B),则线程 A 中该易失性字段之前写入的所有字段都可用于线程 B。据此,如果我修改 getY() 方法上面的共享对象为:
public int getXPlusY() {
int local = x;
return local + y;
}
是否是此操作使 y 对其他线程可见?
我们先看你的第二个例子。
class Shared {
private int y = 0;
private volatile int x = 0;
public void setOne() {
y = 1; //(1)
x = 1; //(2)
}
public int getXPlusY() {
int local = x; //(3)
return local + y; //(4)
}
}
由于程序顺序,我们知道 (1) 和 (2) 之间存在 happens-before 关系:
If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).
由于 x
是易变的,我们知道 (2) 和 (3)
A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.
由于程序顺序,(3)和(4)之间又存在happens-before关系:
If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).
因此,我们有一个 happens-before 链 (1) → (2), (2) → (3), (3) → (4)
并且由于 happens-before 是一个传递关系(如果 A 发生在 B 之前并且 B 发生在 C 之前,那么 A 发生在 C 之前)这意味着 (1) 与 (4).
有 happens-before 关系现在让我们看第一个例子:
class Shared {
private int y = 0;
private volatile int x = 0;
public void setOne() {
y = 1; //(1)
x = 1; //(2)
}
public int getY() {
return y; //(3)
}
}
(1) 和 (2) 之间又存在 happens-before 关系,仅此而已。因为 x
没有被第二个线程读取,所以我们在 (2) 和 (3) 之间没有 happens-before。因此我们在 (1) 和 (3).
我将重命名 类 以使其清楚:
class First {
private int y = 0;
private volatile int x = 0;
public void setOne() {
y = 1;
x = 1;
}
public int getY() {
return y;
}
}
而你的前提条件是:
Assuming we can guarantee that the first thread has started
getY
将 return 1
的保证为零。仅当您遵守 JLS 的 happens-before 规则时,才能保证对共享字段的更新对某些阅读线程可见,此处强制执行 none。因此从 getY
得到 0
是完全有效的。
你的第二个例子:
class Second {
private int y = 0;
private volatile int x = 0;
public void setOne() {
y = 1;
x = 1;
}
public int getXPlusY() {
int local = x;
return local + y;
}
}
同样前提条件是:
Assuming we can guarantee that the first thread has started
不保证return2
。对于输出 2
的方法,读取线程必须观察(查看)volatile 写入。也就是说,它必须遵循以下原则:
public int getXPlusY() {
int local = x;
if(local == 1){ // you need to see the write that was done in the other thread
return local + y;
}
return local + y;
}
如果您输入 if 语句 f(local == 1)
,您会观察到在另一个线程 (x = 1
) 中完成的易失性写入,因此,您可以保证之前完成的所有操作那写也是可见的。这意味着在这个时间点,肯定 y = 1
,所以你的方法将 return 2
.
如果没有这个 if 语句,你的阅读 int local = x
是一个活泼的阅读,没有什么可以阻止这个线程在两者之间做 int local = x
:
y = 1;
x = 1;
事实证明这也是可以证明的,这是一个 jcstress
证明另一个答案(至少 1/2)错误的测试:
//
@JCStressTest
@State
@Outcome(id = "0", expect = Expect.FORBIDDEN, desc = "can not happen")
@Outcome(id = "1", expect = Expect.FORBIDDEN, desc = "can not happen")
@Outcome(id = "2", expect = Expect.ACCEPTABLE, desc = "reader thread was done before writer did anything")
@Outcome(id = "3", expect = Expect.ACCEPTABLE_INTERESTING, desc = "racy read!!!")
@Outcome(id = "4", expect = Expect.ACCEPTABLE, desc = "reader thread sees everything that writer did")
public class VolatileRace {
// change defaults so that "readerThread" does not output a false positive.
// it can happen when it runs it's entire method body before "writerThread"
// does anything at all.
private int y = 1;
private volatile int x = 1;
@Actor
public void writerThread() {
y = 2;
x = 2;
}
@Actor
public void readerThread(I_Result result) {
int local = x;
result.r1 = local + y;
}
}
这个测试的重点是,它有这样的输出:
0 0 FORBIDDEN can not happen
1 0 FORBIDDEN can not happen
2 4,616,926 ACCEPTABLE reader thread was done before writer did anything
3 6,694 ACCEPTABLE_INTERESTING racy read!!!
4 3,107,831 ACCEPTABLE reader thread sees everything that writer did
即使看不懂,也说明读线程看到了3
的结果,也就是说读到了x = 1
和y = 2
。