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

问题描述

在阅读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 1Thread 2,通过 volatile 字段 v1
  • 然后不安全地发布:从Thread 2Thread 3通过v2

问题是:Thread 3 能否将 o 视为部分构造的(即 o.f1 == 0)?

Tom Hawtin - tackline 说它可以: Thread 3 可以将 o 视为部分构造的,因为 o.f1 = 1 和 {Thread 1 之间没有发生之前的关系由于发布不安全,{1}} 在 r1 = v2.f1

说句公道话,这让我很惊讶:直到那一刻,我认为第一个安全的出版物就足够了。
据我了解,有效不可变对象(在《Effective Java》和《Java Concurrency in Practice》等流行书籍中有所描述)也受到该问题的影响。

根据happens-before consistency in the JMM,汤姆的解释对我来说似乎完全有效。
但是还有 the causality part in the JMM,它在发生之前添加了约束。所以,也许,因果关系部分可以保证第一次安全发布就足够了。
(我不能说我完全理解因果关系部分,但我想我会理解提交集和执行的示例)。

所以我有两个相关的问题:

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

解决方法

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

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

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

很可能是因为我的答案基于 JIT 为我的测试程序生成的程序集,而且,由于我不是 JIT 专家,如果 JIT 生成的程序集完全不同,我也不会感到惊讶别人电脑上的机器码。


对于测试,我在 ARMv8 上使用了 OpenJDK 64 位服务器 VM(构建 11.0.9+11-alpine-r1,混合模式)。
选择 ARMv8 是因为它具有 a very relaxed memory model,这需要发布者和阅读者线程中的内存屏障指令(与 x86 不同)。

1.通过 volatile 变量的初始发布:很可能是安全的

测试java程序就像问题中一样(我只添加了一个线程来查看为易失性写入生成了哪些汇编代码):

@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,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,#16]
  0x0000fff788f69308: add       sp,#0x20
  0x0000fff788f6930c: ldr       x8,#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,#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,#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,#48]
  0x0000fff781a604c4: add       sp,#0x40
  0x0000fff781a604c8: ldr       x8,#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,#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,#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,#16]
  0x0000fff788f69680: add       sp,#0x20
  0x0000fff788f69684: ldr       x8,#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的内存地址)+(o内的字段f1的偏移量)读取Obj1的值

o.f1 读取的内存地址是根据 s.v2 读取返回的值计算得出的 - 这就是所谓的“地址依赖”。

在 ARMv8 上,这种地址依赖会阻止这两次读取的重新排序(请参阅 Modelling the ARMv8 architecture,operationally: concurrency and ISA 中的 MP+dmb.sy+addr 示例,您可以在 ARM's Memory Model Tool 中自己尝试)—因此我们保证会看到 { {1}} 完全初始化。

v2 中的内存屏障指令有不同的用途:它们防止将 runVolT3 的易失性读取与线程内的其他操作重新排序(在 Java 中,易失性读取是同步操作之一,必须完全有序)。

更重要的是,今天结果证明 all the supported by OpenJDK architectures 地址依赖阻止读取重新排序(请参阅 this table in wiki 中的“依赖加载可以重新排序”或 The JSR-133 Cookbook for Compiler Writers 中的表中的“数据依赖顺序加载?” {3}})。

因此,今天在 OpenJDK 上,如果一个对象最初是通过 volatile 字段发布的,那么即使在不安全的重新发布之后,它也很可能显示为完全初始化。

2.通过同步块的初始发布:很可能不安全

通过同步块完成初始发布时的情况有所不同:

s.v1

这里为 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 的无序写入。

一般来说,在这种情况下不安全的重新发布在今天的 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,它给出该结果并且可以使用 {{3} 中给出的算法验证 }}。

首先让我们根据算法中定义的 17.4.8. Executions and Causality Requirementsvariables 重写我们的 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 代表易失性写入和读取
  • 操作的读/写值显示在 =
  • 之后
  • W[o.f1]=0 处于初始操作中,因为根据 actions

    将默认值(零、假或空)写入每个变量与每个线程中的第一个操作同步。
    虽然在分配包含变量的对象之前将默认值写入变量似乎有点奇怪,但从概念上讲,每个对象都是在程序开始时使用其默认初始化值创建的。 >

这是一种更紧凑的 E 形式:

W[v1]=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

根据the JLS

一个格式良好的执行 E = 通过提交来自 A 的动作来验证。如果 A 中的所有动作都可以提交,那么执行满足Java 编程语言内存模型的因果关系要求。

因此我们需要逐步构建已提交操作的集合(我们得到一个序列 C₀,C₁,... ,其中 Cₖ 是第 k 次迭代的已提交操作集合,而 { {1}}) 直到我们提交 Cₖ ⊆ Cₖ₊₁ 执行 A 的所有操作。
17.4.8. Executions and Causality Requirements 还包含 9 条规则,这些规则定义了我何时可以提交操作。

  • 第 0 步:算法总是从一个空集开始。

    E
  • 第 1 步:我们只提交写入。
    原因是根据规则 7,在 C₀ = ∅ 中提交的读取必须返回来自 Сₖ 的写入,但我们有空的 Сₖ₋₁

    C₀
  • 第 2 步:现在我们可以在线程 2 中提交 E₁: W[v1]=null,W[o.f1]=0 ---------------------------------- W[o.f1]=1 | | Wv[v1]=o | | C₁ = { W[v1]=null,W[o.f1]=0,W[o.f1]=1,Wv[v1]=o } 的读取和写入。
    由于 o 是可变的,v1 发生在 Wv[v1]=o 之前,并且读取返回 Rv[v1]

    o
  • 第 3 步:现在我们已经提交了 E₂: W[v1]=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
    根据规则 6,当前提交的读取只能返回一个发生之前的写入(该值可以在下一步更改为一个不规则的写入)。
    R[v2]R[v2] 的顺序不是先发生,所以 W[v2]=o 读作 R[v2]

    null
  • 第 4 步:现在 E₃: W[v1]=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 } 可以通过数据竞争读取 R[v2],这使得 W[v2]=o 成为可能。
    R[o.f1] 读取默认值 R[o.f1],算法结束,因为我们执行的所有操作都已提交。

    0

结果,我们验证了一个产生 E = E₄: W[v1]=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 } 的执行,因此,这个结果是有效的。


另外,值得注意的是,这种因果关系验证算法几乎没有对happens-before增加额外的限制。
Jeremy Manson(JMM 的作者之一)the JLS section 该算法的存在是为了防止一种相当奇怪的行为 - 当存在导致彼此的循环动作链时(即,当一个动作导致自身时,所谓的“因果循环” ).
在除这些因果循环之外的所有其他情况下,我们都使用了发生在 explains 中的之前。