线程是否可以先通过安全发布获取对象,然后再进行不安全发布?
Can a thread first acquire an object via safe publication and then publish it unsafely?
阅读 this answer 后我想到了这个问题。
代码示例:
class Obj1 {
int f1 = 0;
}
volatile Obj1 v1;
Obj1 v2;
Thread 1 | Thread 2 | Thread 3
-------------------------------------------------
var o = new Obj1(); | |
o.f1 = 1; | |
v1 = o; | |
| v2 = v1; |
| | var r1 = v2.f1;
Is (r1 == 0) possible?
此处对象o
:
- 首次安全发布:从
Thread 1
到 Thread 2
通过 volatile
字段 v1
- 然后不安全地发布:从
Thread 2
到 Thread 3
通过 v2
问题是:Thread 3
能否将 o
视为部分构建(即 o.f1 == 0
)?
Tom Hawtin - tackline 说它可以:Thread 3
可以将 o
视为部分构造的,因为 o.f1 = 1
与 Thread 1
之间没有先行关系和 r1 = v2.f1
在 Thread 3
由于不安全的发布。
公平地说,这让我感到惊讶:直到那一刻我认为第一次安全发布就足够了。
据我了解,effectively immutable 对象(在诸如 Effective Java 和 Java Concurrency in Practice 等流行书籍中有所描述)也受到该问题的影响。
根据 happens-before consistency in the JMM,Tom 的解释对我来说似乎完全正确。
但是还有 the causality part in the JMM,它在 happens-before 之上添加了约束。所以,也许,因果关系部分以某种方式保证第一次安全发布就足够了。
(我不能说我完全理解因果关系部分,但我想我会理解带有提交集和执行的示例)。
所以我有 2 个相关问题:
- Causality part of the JMM 允许还是禁止
Thread 3
将 o
视为部分构建?
- 是否还有其他原因允许或禁止
Thread 3
将 o
视为部分构建?
部分答案:“不安全的重新发布”如何在今天的 OpenJDK 上运行。
(这不是我想得到的最终一般答案,但至少它显示了对最受欢迎的 Java 实现的期望)
简而言之,这取决于对象最初是如何发布的:
- 如果初始发布是通过可变变量完成的,那么“不安全的重新发布”很可能是安全的,即您很可能永远不会将对象视为部分构造的
- 如果初始发布是通过同步块完成的,那么“不安全的重新发布”很可能不安全,即您很可能能够将对象视为部分构造的
很可能是因为我的答案是基于 JIT 为我的测试程序生成的程序集,而且,由于我不是 JIT 方面的专家,所以我不会感到惊讶如果 JIT 在别人的计算机上生成了完全不同的机器代码。
为了进行测试,我在 ARMv8 上使用了 OpenJDK 64 位服务器 VM(构建 11.0.9+11-alpine-r1,混合模式)。
选择 ARMv8 是因为它具有 a very relaxed memory model,这需要发布者和 reader 线程中的内存屏障指令(与 x86 不同)。
1.通过 volatile 变量进行初始发布:很可能是安全的
测试java程序如题(我只加了一个线程,看看为volatile write生成了什么汇编代码):
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(value = 1,
jvmArgsAppend = {"-Xmx512m", "-server", "-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintAssembly",
"-XX:+PrintInterpreter", "-XX:+PrintNMethods", "-XX:+PrintNativeNMethods",
"-XX:+PrintSignatureHandlers", "-XX:+PrintAdapterHandlers", "-XX:+PrintStubCode",
"-XX:+PrintCompilation", "-XX:+PrintInlining", "-XX:+TraceClassLoading",})
@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(4)
public class VolTest {
static class Obj1 {
int f1 = 0;
}
@State(Scope.Group)
public static class State1 {
volatile Obj1 v1 = new Obj1();
Obj1 v2 = new Obj1();
}
@Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void runVolT1(State1 s) {
Obj1 o = new Obj1(); /* 43 */
o.f1 = 1; /* 44 */
s.v1 = o; /* 45 */
}
@Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void runVolT2(State1 s) {
s.v2 = s.v1; /* 52 */
}
@Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int runVolT3(State1 s) {
return s.v1.f1; /* 59 */
}
@Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int runVolT4(State1 s) {
return s.v2.f1; /* 66 */
}
}
这是 JIT 为 runVolT3
和 runVolT4
生成的程序集:
Compiled method (c1) 26806 529 2 org.sample.VolTest::runVolT3 (8 bytes)
...
[Constants]
# {method} {0x0000fff77cbc4f10} 'runVolT3' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
# this: c_rarg1:c_rarg1
= 'org/sample/VolTest'
# parm0: c_rarg2:c_rarg2
= 'org/sample/VolTest$State1'
...
[Verified Entry Point]
...
;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@0 (line 59)
0x0000fff781a60938: dmb ish
0x0000fff781a6093c: ldr w0, [x2, #12] ; implicit exception: dispatches to 0x0000fff781a60984
0x0000fff781a60940: dmb ishld ;*getfield v1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@1 (line 59)
0x0000fff781a60944: ldr w0, [x0, #12] ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@4 (line 59)
; implicit exception: dispatches to 0x0000fff781a60990
0x0000fff781a60948: ldp x29, x30, [sp, #48]
0x0000fff781a6094c: add sp, sp, #0x40
0x0000fff781a60950: ldr x8, [x28, #264]
0x0000fff781a60954: ldr wzr, [x8] ; {poll_return}
0x0000fff781a60958: ret
...
Compiled method (c2) 27005 536 4 org.sample.VolTest::runVolT3 (8 bytes)
...
[Constants]
# {method} {0x0000fff77cbc4f10} 'runVolT3' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
# this: c_rarg1:c_rarg1
= 'org/sample/VolTest'
# parm0: c_rarg2:c_rarg2
= 'org/sample/VolTest$State1'
...
[Verified Entry Point]
...
; - org.sample.VolTest::runVolT3@-1 (line 59)
0x0000fff788f692f4: cbz x2, 0x0000fff788f69318
0x0000fff788f692f8: add x10, x2, #0xc
0x0000fff788f692fc: ldar w11, [x10] ;*getfield v1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@1 (line 59)
0x0000fff788f69300: ldr w0, [x11, #12] ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@4 (line 59)
; implicit exception: dispatches to 0x0000fff788f69320
0x0000fff788f69304: ldp x29, x30, [sp, #16]
0x0000fff788f69308: add sp, sp, #0x20
0x0000fff788f6930c: ldr x8, [x28, #264]
0x0000fff788f69310: ldr wzr, [x8] ; {poll_return}
0x0000fff788f69314: ret
...
Compiled method (c1) 26670 527 2 org.sample.VolTest::runVolT4 (8 bytes)
...
[Constants]
# {method} {0x0000fff77cbc4ff0} 'runVolT4' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
# this: c_rarg1:c_rarg1
= 'org/sample/VolTest'
# parm0: c_rarg2:c_rarg2
= 'org/sample/VolTest$State1'
...
[Verified Entry Point]
...
;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@0 (line 66)
0x0000fff781a604b8: ldr w0, [x2, #16] ;*getfield v2 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@1 (line 66)
; implicit exception: dispatches to 0x0000fff781a604fc
0x0000fff781a604bc: ldr w0, [x0, #12] ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@4 (line 66)
; implicit exception: dispatches to 0x0000fff781a60508
0x0000fff781a604c0: ldp x29, x30, [sp, #48]
0x0000fff781a604c4: add sp, sp, #0x40
0x0000fff781a604c8: ldr x8, [x28, #264]
0x0000fff781a604cc: ldr wzr, [x8] ; {poll_return}
0x0000fff781a604d0: ret
...
Compiled method (c2) 27497 535 4 org.sample.VolTest::runVolT4 (8 bytes)
...
[Constants]
# {method} {0x0000fff77cbc4ff0} 'runVolT4' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
# this: c_rarg1:c_rarg1
= 'org/sample/VolTest'
# parm0: c_rarg2:c_rarg2
= 'org/sample/VolTest$State1'
...
[Verified Entry Point]
...
; - org.sample.VolTest::runVolT4@-1 (line 66)
0x0000fff788f69674: ldr w11, [x2, #16] ;*getfield v2 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@1 (line 66)
; implicit exception: dispatches to 0x0000fff788f69690
0x0000fff788f69678: ldr w0, [x11, #12] ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@4 (line 66)
; implicit exception: dispatches to 0x0000fff788f69698
0x0000fff788f6967c: ldp x29, x30, [sp, #16]
0x0000fff788f69680: add sp, sp, #0x20
0x0000fff788f69684: ldr x8, [x28, #264]
0x0000fff788f69688: ldr wzr, [x8] ; {poll_return}
0x0000fff788f6968c: ret
让我们注意 barrier instructions 生成的程序集包含的内容:
runVolT1
(上面没有显示程序集,因为它太长了):
c1
版本包含 1x dmb ishst
、2x dmb ish
c2
版本包含 1x dmb ishst
、1x dmb ish
、1x stlr
runVolT3
(读作 volatile v1
):
c1
版本 1x dmb ish
、1x dmb ishld
c2
版本 1x ldar
runVolT4
(读取非易失性v2
):没有内存障碍
如您所见,runVolT4
(在不安全的重新发布后读取对象)不包含内存屏障。
这是否意味着线程可以看到对象状态为半初始化?
事实证明不,在 ARMv8 上它仍然是安全的。
为什么?
查看代码中的return s.v2.f1;
。这里CPU执行2次内存读取:
- 首先读取
s.v2
,其中包含对象o
的内存地址
- 然后它从(
o
的内存地址)+(字段f1
在Obj1
中的偏移)读取o.f1
的值
o.f1
读取的内存地址是根据 s.v2
读取返回的值计算的——这就是所谓的“地址依赖性”。
在 ARMv8 上,这种地址依赖性会阻止这两个读取的重新排序(请参阅 Modelling the ARMv8 architecture, operationally: concurrency and ISA, you can try it yourself in ARM's Memory Model Tool 中的 MP+dmb.sy+addr
示例)——因此我们可以保证看到 v2
已完全初始化。
runVolT3
中的内存屏障指令有不同的用途:它们防止 s.v1
的易失性读取与线程内的其他操作重新排序(在 Java 中,易失性读取是其中之一同步动作,必须完全有序)。
不止于此,结果是今天 all the supported by OpenJDK architectures address dependency prevents reordering of reads (see "Dependent loads can be reordered" in this table in wiki or "Data dependency orders loads?" in table in The JSR-133 Cookbook for Compiler Writers)。
因此,今天在 OpenJDK 上,如果一个对象最初是通过 volatile 字段发布的,那么即使在不安全的重新发布之后,它也很可能会显示为已完全初始化。
2。通过同步块的初始发布:很可能是不安全的
通过同步块完成初始发布时情况不同:
class Obj1 {
int f1 = 0;
}
Obj1 v1;
Obj1 v2;
Thread 1 | Thread 2 | Thread 3
--------------------------------------------------------
synchronized { | |
var o = new Obj1(); | |
o.f1 = 1; | |
v1 = o; | |
} | |
| synchronized { |
| var r1 = v1; |
| } |
| v2 = r1; |
| | var r2 = v2.f1;
Is (r2 == 0) possible?
此处为 Thread 3
生成的程序集与上面为 runVolT4
生成的程序集相同:它不包含内存屏障指令。
因此,Thread 3
可以很容易地看到来自 Thread 1
的写入乱序。
一般来说,在这种情况下,不安全的重新发布在今天的 OpenJDK 上很可能是不安全的。
答案:Causality part of the JMM 允许 Thread 3
将 o
视为部分构建。
我终于设法将 17.4.8. Executions and Causality Requirements (aka the causality part of the JMM) 应用于此示例。
所以这是我们的 Java 程序:
class Obj1 {
int f1;
}
volatile Obj1 v1;
Obj1 v2;
Thread 1 | Thread 2 | Thread 3
--------------------|----------|-----------------
var o = new Obj1(); | |
o.f1 = 1; | |
v1 = o; | |
| v2 = v1; |
| | var r1 = v2.f1;
我们想知道结果 (r1 == 0)
是否被允许。
原来,要证明(r1 == 0)
是允许的,我们需要找到a well-formed execution, which gives that result and can be validated with the algorithm given in 17.4.8. Executions and Causality Requirements.
首先让我们根据算法中定义的 variables and actions 重写我们的 Java 程序。
让我们还显示我们的读取和写入操作的值以获得我们想要验证的执行E
:
Initially: W[v1]=null, W[v2]=null, W[o.f1]=0
Thread 1 | Thread 2 | Thread 3
----------|----------|-----------
W[o.f1]=1 | |
Wv[v1]=o | |
| Rv[v1]=o |
| W[v2]=o |
| | R[v2]=o
| | R[o.f1]=0
备注:
o
表示java代码中new Obj1();
创建的实例
W
和R
代表正常的读写; Wv
和 Rv
表示易失性写入和读取
- read/written 操作的值显示在
=
之后
W[o.f1]=0
处于初始操作中,因为根据 the JLS:
The write of the default value (zero, false, or null) to each variable synchronizes-with the first action in every thread.
Although it may seem a little strange to write a default value to a variable before the object containing the variable is allocated, conceptually every object is created at the start of the program with its default initialized values.
这里是 E
更紧凑的形式:
W[v1]=null, W[v2]=null, W[o.f1]=0
---------------------------------
W[o.f1]=1 | |
Wv[v1]=o | |
| Rv[v1]=o |
| W[v2]=o |
| | R[v2]=o
| | R[o.f1]=0
验证 E
根据17.4.8. Executions and Causality Requirements:
A well-formed execution E = < P, A, po, so, W, V, sw, hb > is validated by committing actions from A. If all of the actions in A can be committed, then the execution satisfies the causality requirements of the Java programming language memory model.
因此我们需要逐步构建已提交操作集(我们得到一个序列 C₀,C₁,...
,其中 Cₖ
是第 k 次迭代的已提交操作集,和 Cₖ ⊆ Cₖ₊₁
) 直到我们提交执行的所有操作 A
E
.
the JLS section 还包含 9 条规则,这些规则定义了我何时可以执行操作。
第 0 步:算法总是从空集开始。
C₀ = ∅
第 1 步:我们只提交写入。
原因是根据规则 7,在 Сₖ
中提交的读取必须 return 从 Сₖ₋₁
中写入,但我们有空的 C₀
.
E₁:
W[v1]=null, W[v2]=null, W[o.f1]=0
----------------------------------
W[o.f1]=1 | |
Wv[v1]=o | |
C₁ = { W[v1]=null, W[v2]=null, W[o.f1]=0, W[o.f1]=1, Wv[v1]=o }
第 2 步:现在我们可以在线程 2 中提交对 o
的读取和写入。
由于 v1
是易变的,Wv[v1]=o
发生在 Rv[v1]
之前,并且读取 returns o
.
E₂:
W[v1]=null, W[v2]=null, W[o.f1]=0
---------------------------------
W[o.f1]=1 | |
Wv[v1]=o | |
| Rv[v1]=o |
| W[v2]=o |
C₂ = C₁∪{ Rv[v1]=o, W[v2]=o }
第 3 步:现在我们已经 W[v2]=o
提交,我们可以在线程 3 中提交读取 R[v2]
。
根据规则 6,当前提交的读取只能 return 发生在写入之前(该值可以在下一步更改一次为活泼写入)。
R[v2]
和 W[v2]=o
未按 happens-before 排序,因此 R[v2]
读取 null
。
E₃:
W[v1]=null, W[v2]=null, W[o.f1]=0
---------------------------------
W[o.f1]=1 | |
Wv[v1]=o | |
| Rv[v1]=o |
| W[v2]=o |
| | R[v2]=null
C₃ = C₂∪{ R[v2]=null }
第 4 步:现在 R[v2]
可以通过数据竞争读取 W[v2]=o
,这使得 R[o.f1]
成为可能。
R[o.f1]
读取默认值 0
,算法结束,因为我们执行的所有操作都已提交。
E = E₄:
W[v1]=null, W[v2]=null, W[o.f1]=0
---------------------------------
W[o.f1]=1 | |
Wv[v1]=o | |
| Rv[v1]=o |
| W[v2]=o |
| | R[v2]=o
| | R[o.f1]=0
A = C₄ = C₂∪{ R[v2]=o, R[o.f1]=0 }
因此,我们验证了一个产生 (r1 == 0)
的执行,因此,这个结果是有效的。
此外,值得注意的是,这种因果关系验证算法几乎没有对 happens-before 添加额外的限制。
Jeremy Manson(JMM 的作者之一)explains 该算法的存在是为了防止一种相当奇怪的行为——所谓的“因果循环”,当存在一个循环的动作链时会导致彼此(即当一个动作导致自身时).
在除了这些因果循环之外的所有其他情况下,我们使用 happens-before 就像 the Tom's comment.
阅读 this answer 后我想到了这个问题。
代码示例:
class Obj1 {
int f1 = 0;
}
volatile Obj1 v1;
Obj1 v2;
Thread 1 | Thread 2 | Thread 3
-------------------------------------------------
var o = new Obj1(); | |
o.f1 = 1; | |
v1 = o; | |
| v2 = v1; |
| | var r1 = v2.f1;
Is (r1 == 0) possible?
此处对象o
:
- 首次安全发布:从
Thread 1
到Thread 2
通过volatile
字段v1
- 然后不安全地发布:从
Thread 2
到Thread 3
通过v2
问题是:Thread 3
能否将 o
视为部分构建(即 o.f1 == 0
)?
Tom Hawtin - tackline 说它可以:Thread 3
可以将 o
视为部分构造的,因为 o.f1 = 1
与 Thread 1
之间没有先行关系和 r1 = v2.f1
在 Thread 3
由于不安全的发布。
公平地说,这让我感到惊讶:直到那一刻我认为第一次安全发布就足够了。
据我了解,effectively immutable 对象(在诸如 Effective Java 和 Java Concurrency in Practice 等流行书籍中有所描述)也受到该问题的影响。
根据 happens-before consistency in the JMM,Tom 的解释对我来说似乎完全正确。
但是还有 the causality part in the JMM,它在 happens-before 之上添加了约束。所以,也许,因果关系部分以某种方式保证第一次安全发布就足够了。
(我不能说我完全理解因果关系部分,但我想我会理解带有提交集和执行的示例)。
所以我有 2 个相关问题:
- Causality part of the JMM 允许还是禁止
Thread 3
将o
视为部分构建? - 是否还有其他原因允许或禁止
Thread 3
将o
视为部分构建?
部分答案:“不安全的重新发布”如何在今天的 OpenJDK 上运行。
(这不是我想得到的最终一般答案,但至少它显示了对最受欢迎的 Java 实现的期望)
简而言之,这取决于对象最初是如何发布的:
- 如果初始发布是通过可变变量完成的,那么“不安全的重新发布”很可能是安全的,即您很可能永远不会将对象视为部分构造的
- 如果初始发布是通过同步块完成的,那么“不安全的重新发布”很可能不安全,即您很可能能够将对象视为部分构造的
很可能是因为我的答案是基于 JIT 为我的测试程序生成的程序集,而且,由于我不是 JIT 方面的专家,所以我不会感到惊讶如果 JIT 在别人的计算机上生成了完全不同的机器代码。
为了进行测试,我在 ARMv8 上使用了 OpenJDK 64 位服务器 VM(构建 11.0.9+11-alpine-r1,混合模式)。
选择 ARMv8 是因为它具有 a very relaxed memory model,这需要发布者和 reader 线程中的内存屏障指令(与 x86 不同)。
1.通过 volatile 变量进行初始发布:很可能是安全的
测试java程序如题(我只加了一个线程,看看为volatile write生成了什么汇编代码):
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(value = 1,
jvmArgsAppend = {"-Xmx512m", "-server", "-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintAssembly",
"-XX:+PrintInterpreter", "-XX:+PrintNMethods", "-XX:+PrintNativeNMethods",
"-XX:+PrintSignatureHandlers", "-XX:+PrintAdapterHandlers", "-XX:+PrintStubCode",
"-XX:+PrintCompilation", "-XX:+PrintInlining", "-XX:+TraceClassLoading",})
@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(4)
public class VolTest {
static class Obj1 {
int f1 = 0;
}
@State(Scope.Group)
public static class State1 {
volatile Obj1 v1 = new Obj1();
Obj1 v2 = new Obj1();
}
@Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void runVolT1(State1 s) {
Obj1 o = new Obj1(); /* 43 */
o.f1 = 1; /* 44 */
s.v1 = o; /* 45 */
}
@Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void runVolT2(State1 s) {
s.v2 = s.v1; /* 52 */
}
@Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int runVolT3(State1 s) {
return s.v1.f1; /* 59 */
}
@Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int runVolT4(State1 s) {
return s.v2.f1; /* 66 */
}
}
这是 JIT 为 runVolT3
和 runVolT4
生成的程序集:
Compiled method (c1) 26806 529 2 org.sample.VolTest::runVolT3 (8 bytes)
...
[Constants]
# {method} {0x0000fff77cbc4f10} 'runVolT3' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
# this: c_rarg1:c_rarg1
= 'org/sample/VolTest'
# parm0: c_rarg2:c_rarg2
= 'org/sample/VolTest$State1'
...
[Verified Entry Point]
...
;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@0 (line 59)
0x0000fff781a60938: dmb ish
0x0000fff781a6093c: ldr w0, [x2, #12] ; implicit exception: dispatches to 0x0000fff781a60984
0x0000fff781a60940: dmb ishld ;*getfield v1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@1 (line 59)
0x0000fff781a60944: ldr w0, [x0, #12] ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@4 (line 59)
; implicit exception: dispatches to 0x0000fff781a60990
0x0000fff781a60948: ldp x29, x30, [sp, #48]
0x0000fff781a6094c: add sp, sp, #0x40
0x0000fff781a60950: ldr x8, [x28, #264]
0x0000fff781a60954: ldr wzr, [x8] ; {poll_return}
0x0000fff781a60958: ret
...
Compiled method (c2) 27005 536 4 org.sample.VolTest::runVolT3 (8 bytes)
...
[Constants]
# {method} {0x0000fff77cbc4f10} 'runVolT3' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
# this: c_rarg1:c_rarg1
= 'org/sample/VolTest'
# parm0: c_rarg2:c_rarg2
= 'org/sample/VolTest$State1'
...
[Verified Entry Point]
...
; - org.sample.VolTest::runVolT3@-1 (line 59)
0x0000fff788f692f4: cbz x2, 0x0000fff788f69318
0x0000fff788f692f8: add x10, x2, #0xc
0x0000fff788f692fc: ldar w11, [x10] ;*getfield v1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@1 (line 59)
0x0000fff788f69300: ldr w0, [x11, #12] ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@4 (line 59)
; implicit exception: dispatches to 0x0000fff788f69320
0x0000fff788f69304: ldp x29, x30, [sp, #16]
0x0000fff788f69308: add sp, sp, #0x20
0x0000fff788f6930c: ldr x8, [x28, #264]
0x0000fff788f69310: ldr wzr, [x8] ; {poll_return}
0x0000fff788f69314: ret
...
Compiled method (c1) 26670 527 2 org.sample.VolTest::runVolT4 (8 bytes)
...
[Constants]
# {method} {0x0000fff77cbc4ff0} 'runVolT4' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
# this: c_rarg1:c_rarg1
= 'org/sample/VolTest'
# parm0: c_rarg2:c_rarg2
= 'org/sample/VolTest$State1'
...
[Verified Entry Point]
...
;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@0 (line 66)
0x0000fff781a604b8: ldr w0, [x2, #16] ;*getfield v2 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@1 (line 66)
; implicit exception: dispatches to 0x0000fff781a604fc
0x0000fff781a604bc: ldr w0, [x0, #12] ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@4 (line 66)
; implicit exception: dispatches to 0x0000fff781a60508
0x0000fff781a604c0: ldp x29, x30, [sp, #48]
0x0000fff781a604c4: add sp, sp, #0x40
0x0000fff781a604c8: ldr x8, [x28, #264]
0x0000fff781a604cc: ldr wzr, [x8] ; {poll_return}
0x0000fff781a604d0: ret
...
Compiled method (c2) 27497 535 4 org.sample.VolTest::runVolT4 (8 bytes)
...
[Constants]
# {method} {0x0000fff77cbc4ff0} 'runVolT4' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
# this: c_rarg1:c_rarg1
= 'org/sample/VolTest'
# parm0: c_rarg2:c_rarg2
= 'org/sample/VolTest$State1'
...
[Verified Entry Point]
...
; - org.sample.VolTest::runVolT4@-1 (line 66)
0x0000fff788f69674: ldr w11, [x2, #16] ;*getfield v2 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@1 (line 66)
; implicit exception: dispatches to 0x0000fff788f69690
0x0000fff788f69678: ldr w0, [x11, #12] ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@4 (line 66)
; implicit exception: dispatches to 0x0000fff788f69698
0x0000fff788f6967c: ldp x29, x30, [sp, #16]
0x0000fff788f69680: add sp, sp, #0x20
0x0000fff788f69684: ldr x8, [x28, #264]
0x0000fff788f69688: ldr wzr, [x8] ; {poll_return}
0x0000fff788f6968c: ret
让我们注意 barrier instructions 生成的程序集包含的内容:
runVolT1
(上面没有显示程序集,因为它太长了):c1
版本包含 1xdmb ishst
、2xdmb ish
c2
版本包含 1xdmb ishst
、1xdmb ish
、1xstlr
runVolT3
(读作 volatilev1
):c1
版本 1xdmb ish
、1xdmb ishld
c2
版本 1xldar
runVolT4
(读取非易失性v2
):没有内存障碍
如您所见,runVolT4
(在不安全的重新发布后读取对象)不包含内存屏障。
这是否意味着线程可以看到对象状态为半初始化?
事实证明不,在 ARMv8 上它仍然是安全的。
为什么?
查看代码中的return s.v2.f1;
。这里CPU执行2次内存读取:
- 首先读取
s.v2
,其中包含对象o
的内存地址
- 然后它从(
o
的内存地址)+(字段f1
在Obj1
中的偏移)读取o.f1
的值
o.f1
读取的内存地址是根据 s.v2
读取返回的值计算的——这就是所谓的“地址依赖性”。
在 ARMv8 上,这种地址依赖性会阻止这两个读取的重新排序(请参阅 Modelling the ARMv8 architecture, operationally: concurrency and ISA, you can try it yourself in ARM's Memory Model Tool 中的 MP+dmb.sy+addr
示例)——因此我们可以保证看到 v2
已完全初始化。
runVolT3
中的内存屏障指令有不同的用途:它们防止 s.v1
的易失性读取与线程内的其他操作重新排序(在 Java 中,易失性读取是其中之一同步动作,必须完全有序)。
不止于此,结果是今天 all the supported by OpenJDK architectures address dependency prevents reordering of reads (see "Dependent loads can be reordered" in this table in wiki or "Data dependency orders loads?" in table in The JSR-133 Cookbook for Compiler Writers)。
因此,今天在 OpenJDK 上,如果一个对象最初是通过 volatile 字段发布的,那么即使在不安全的重新发布之后,它也很可能会显示为已完全初始化。
2。通过同步块的初始发布:很可能是不安全的
通过同步块完成初始发布时情况不同:
class Obj1 {
int f1 = 0;
}
Obj1 v1;
Obj1 v2;
Thread 1 | Thread 2 | Thread 3
--------------------------------------------------------
synchronized { | |
var o = new Obj1(); | |
o.f1 = 1; | |
v1 = o; | |
} | |
| synchronized { |
| var r1 = v1; |
| } |
| v2 = r1; |
| | var r2 = v2.f1;
Is (r2 == 0) possible?
此处为 Thread 3
生成的程序集与上面为 runVolT4
生成的程序集相同:它不包含内存屏障指令。
因此,Thread 3
可以很容易地看到来自 Thread 1
的写入乱序。
一般来说,在这种情况下,不安全的重新发布在今天的 OpenJDK 上很可能是不安全的。
答案:Causality part of the JMM 允许 Thread 3
将 o
视为部分构建。
我终于设法将 17.4.8. Executions and Causality Requirements (aka the causality part of the JMM) 应用于此示例。
所以这是我们的 Java 程序:
class Obj1 {
int f1;
}
volatile Obj1 v1;
Obj1 v2;
Thread 1 | Thread 2 | Thread 3
--------------------|----------|-----------------
var o = new Obj1(); | |
o.f1 = 1; | |
v1 = o; | |
| v2 = v1; |
| | var r1 = v2.f1;
我们想知道结果 (r1 == 0)
是否被允许。
原来,要证明(r1 == 0)
是允许的,我们需要找到a well-formed execution, which gives that result and can be validated with the algorithm given in 17.4.8. Executions and Causality Requirements.
首先让我们根据算法中定义的 variables and actions 重写我们的 Java 程序。
让我们还显示我们的读取和写入操作的值以获得我们想要验证的执行E
:
Initially: W[v1]=null, W[v2]=null, W[o.f1]=0
Thread 1 | Thread 2 | Thread 3
----------|----------|-----------
W[o.f1]=1 | |
Wv[v1]=o | |
| Rv[v1]=o |
| W[v2]=o |
| | R[v2]=o
| | R[o.f1]=0
备注:
o
表示java代码中new Obj1();
创建的实例W
和R
代表正常的读写;Wv
和Rv
表示易失性写入和读取- read/written 操作的值显示在
=
之后
W[o.f1]=0
处于初始操作中,因为根据 the JLS:The write of the default value (zero, false, or null) to each variable synchronizes-with the first action in every thread.
Although it may seem a little strange to write a default value to a variable before the object containing the variable is allocated, conceptually every object is created at the start of the program with its default initialized values.
这里是 E
更紧凑的形式:
W[v1]=null, W[v2]=null, W[o.f1]=0
---------------------------------
W[o.f1]=1 | |
Wv[v1]=o | |
| Rv[v1]=o |
| W[v2]=o |
| | R[v2]=o
| | R[o.f1]=0
验证 E
根据17.4.8. Executions and Causality Requirements:
A well-formed execution E = < P, A, po, so, W, V, sw, hb > is validated by committing actions from A. If all of the actions in A can be committed, then the execution satisfies the causality requirements of the Java programming language memory model.
因此我们需要逐步构建已提交操作集(我们得到一个序列 C₀,C₁,...
,其中 Cₖ
是第 k 次迭代的已提交操作集,和 Cₖ ⊆ Cₖ₊₁
) 直到我们提交执行的所有操作 A
E
.
the JLS section 还包含 9 条规则,这些规则定义了我何时可以执行操作。
第 0 步:算法总是从空集开始。
C₀ = ∅
第 1 步:我们只提交写入。
原因是根据规则 7,在Сₖ
中提交的读取必须 return 从Сₖ₋₁
中写入,但我们有空的C₀
.E₁: W[v1]=null, W[v2]=null, W[o.f1]=0 ---------------------------------- W[o.f1]=1 | | Wv[v1]=o | | C₁ = { W[v1]=null, W[v2]=null, W[o.f1]=0, W[o.f1]=1, Wv[v1]=o }
第 2 步:现在我们可以在线程 2 中提交对
o
的读取和写入。
由于v1
是易变的,Wv[v1]=o
发生在Rv[v1]
之前,并且读取 returnso
.E₂: W[v1]=null, W[v2]=null, W[o.f1]=0 --------------------------------- W[o.f1]=1 | | Wv[v1]=o | | | Rv[v1]=o | | W[v2]=o | C₂ = C₁∪{ Rv[v1]=o, W[v2]=o }
第 3 步:现在我们已经
W[v2]=o
提交,我们可以在线程 3 中提交读取R[v2]
。
根据规则 6,当前提交的读取只能 return 发生在写入之前(该值可以在下一步更改一次为活泼写入)。
R[v2]
和W[v2]=o
未按 happens-before 排序,因此R[v2]
读取null
。E₃: W[v1]=null, W[v2]=null, W[o.f1]=0 --------------------------------- W[o.f1]=1 | | Wv[v1]=o | | | Rv[v1]=o | | W[v2]=o | | | R[v2]=null C₃ = C₂∪{ R[v2]=null }
第 4 步:现在
R[v2]
可以通过数据竞争读取W[v2]=o
,这使得R[o.f1]
成为可能。
R[o.f1]
读取默认值0
,算法结束,因为我们执行的所有操作都已提交。E = E₄: W[v1]=null, W[v2]=null, W[o.f1]=0 --------------------------------- W[o.f1]=1 | | Wv[v1]=o | | | Rv[v1]=o | | W[v2]=o | | | R[v2]=o | | R[o.f1]=0 A = C₄ = C₂∪{ R[v2]=o, R[o.f1]=0 }
因此,我们验证了一个产生 (r1 == 0)
的执行,因此,这个结果是有效的。
此外,值得注意的是,这种因果关系验证算法几乎没有对 happens-before 添加额外的限制。
Jeremy Manson(JMM 的作者之一)explains 该算法的存在是为了防止一种相当奇怪的行为——所谓的“因果循环”,当存在一个循环的动作链时会导致彼此(即当一个动作导致自身时).
在除了这些因果循环之外的所有其他情况下,我们使用 happens-before 就像 the Tom's comment.