gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-03 11:17:38 24 4
gpt4 key购买 nike

阅读 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 1r1 = v2.f1 中的 Thread 3 之间没有发生之前的关系。
    公平地说,这让我感到惊讶:直到那一刻我认为第一次安全出版物就足够了。
    据我了解,有效的不可变对象(immutable对象)(在 Effective Java 和 Java Concurrency in Practice 等流行书籍中有所描述)也受到该问题的影响。
    根据 happens-before consistency in the JMM ,汤姆的解释对我来说似乎完全有效。
    但是还有 the causality part in the JMM ,它在发生之前添加了约束。所以,也许,因果关系部分以某种方式保证了第一个安全发布就足够了。
    (我不能说我完全理解因果关系部分,但我想我会理解提交集和执行的例子)。
    所以我有两个相关的问题:
  • Causality part of the JMM 是否允许或禁止 Thread 3o 视为部分构造?
  • 是否还有其他原因允许或禁止 Thread 3o 视为部分构造?
  • 最佳答案

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

  • 如果初始发布是通过 volatile 变量完成的,那么“不安全的重新发布”很可能是安全的,即您很可能永远不会看到该对象是部分构造的
  • 如果初始发布是通过同步块(synchronized block)完成的,那么“不安全的重新发布”很可能是不安全的,即您很可能会看到对象是部分构造的

  • 很可能是因为我的答案基于 JIT 为我的测试程序生成的程序集,而且由于我不是 JIT 专家,如果 JIT 在其他人的计算机上生成完全不同的机器代码,我不会感到惊讶。

    对于测试,我在 ARMv8 上使用了 OpenJDK 64 位服务器 VM(构建 11.0.9+11-alpine-r1,混合模式)。
    选择 ARMv8 是因为它具有 a very relaxed memory model ,这需要发布者和阅读者线程中的内存屏障指令(与 x86 不同)。
    1. 通过一个 volatile 变量的初始发布:很可能是安全的
    测试java程序就像问题中的一样(我只添加了一个线程来查看为 volatile 写入生成了哪些汇编代码):
    @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 (读取非 volatile v2 ):没有内存障碍

  • 如您所见, runVolT4 (在不安全的重新发布后读取对象)不包含内存障碍。
    这是否意味着线程可以将对象状态视为半初始化?
    事实证明不是,在 ARMv8 上它仍然是安全的。
    为什么?
    return s.v2.f1;在代码中。这里 CPU 执行 2 次内存读取:
  • 首先是 s.v2 ,其中包含对象o的内存地址
  • 然后它读取 o.f1 的值from(o 的内存地址)+(f1 中的字段Obj1 的偏移量)
  • o.f1 的内存地址read 是根据 s.v2 返回的值计算的读取——这就是所谓的“地址依赖”。
    在 ARMv8 上,这种地址依赖会阻止这两个读取的重新排序(参见 MP+dmb.sy+addr 中的示例 Modelling the ARMv8 architecture, operationally: concurrency and ISA,您可以在 ARM's Memory Model Tool 中自己尝试)——所以我们保证看到 v2完全初始化。 runVolT3 中的内存屏障指令服务于不同的目的:它们防止重新排序 s.v1 的 volatile 读取与线程内的其他操作(在 Java 中, volatile 读取是同步操作之一,必须完全有序)。
    不仅如此,今天的结果是 all the supported by OpenJDK architectures地址依赖阻止读取的重新排序(请参阅 this table in wiki 中的“依赖加载可以重新排序”或 The JSR-133 Cookbook for Compiler Writers 中的表中的“数据依赖顺序加载?”)。
    因此,今天在 OpenJDK 上,如果一个对象最初是通过 volatile 字段发布的,那么即使在不安全的重新发布之后,它也很可能在完全初始化后可见。
    2. 通过同步块(synchronized block)的初始发布:很可能不安全
    通过同步块(synchronized block)完成初始发布时的情况有所不同:
    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 上很可能是不安全的。

    关于java - 线程是否可以先通过安全发布获取对象,然后不安全地发布它?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66214096/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com