线程是否可以先通过安全发布获取对象,然后再进行不安全发布?

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 3 能否将 o 视为部分构建(即 o.f1 == 0)?

Tom Hawtin - tackline 说它可以:Thread 3 可以将 o 视为部分构造的,因为 o.f1 = 1Thread 1 之间没有先行关系和 r1 = v2.f1Thread 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 个相关问题:

  1. Causality part of the JMM 允许还是禁止 Thread 3o 视为部分构建?
  2. 是否还有其他原因允许或禁止 Thread 3o 视为部分构建?

部分答案:“不安全的重新发布”如何在今天的 OpenJDK 上运行。
(这不是我想得到的最终一般答案,但至少它显示了对最受欢迎的 Java 实现的期望)

简而言之,这取决于对象最初是如何发布的:

  1. 如果初始发布是通过可变变量完成的,那么“不安全的重新发布”很可能是安全的,即您很可能永远不会将对象视为部分构造的
  2. 如果初始发布是通过同步块完成的,那么“不安全的重新发布”很可能不安全,即您很可能能够将对象视为部分构造的

很可能是因为我的答案是基于 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 为 runVolT3runVolT4 生成的程序集:

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的内存地址)+(字段f1Obj1中的偏移)读取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 3o 视为部分构建。

我终于设法将 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();创建的实例
  • WR代表正常的读写; WvRv 表示易失性写入和读取
  • 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.