热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

线程能否先以安全方式获取对象,再进行非安全发布?

阅读此答案后,我想到了这个问题。代码示例:class Obj1 { int f1 = 0;}volatile Obj1 v1;Obj1 v2;Thread 1 | Thread

阅读此答案后,我想到了这个问题。

代码示例:

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 = 1inThread 1r1 = v2.f1in之间没有发生之前的关系Thread 3

公平地说,这让我感到惊讶:直到那一刻我认为第一次安全出版物就足够了。

据我了解,有效的不可变对象(在 Effective Java 和 Java Concurrency in Practice 等流行书籍中有所描述)也受到该问题的影响。

根据JMM 中的发生前一致性,汤姆的解释对我来说似乎完全有效。

但是JMM 中也有因果关系部分,它在发生之前添加了约束。所以,也许,因果关系部分以某种方式保证了第一个安全发布就足够了。

(我不能说我完全理解因果关系部分,但我想我会理解提交集和执行的例子)。

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


  1. 不会的JMM的因果关系部分允许或禁止Thread 3看到o的部分构造的?

  2. 是否有任何其他原因Thread 3允许或禁止将其o视为部分建造的?


回答


部分答案:“不安全的重新发布”如何在今天的 OpenJDK 上工作。

(这不是我想得到的最终通用答案,但至少它显示了对最流行的 Java 实现的期望)

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


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

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

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


对于测试,我在 ARMv8 上使用了 OpenJDK 64 位服务器 VM(构建 11.0.9+11-alpine-r1,混合模式)。

选择 ARMv8 是因为它有一个非常宽松的内存模型,它需要发布者和阅读者线程中的内存屏障指令(与 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, 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 为runVolT3and生成的程序集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

让我们注意生成的程序集包含哪些屏障指令:


  • runVolT1 (上面没有显示程序集,因为它太长了):

    • c1版本包含 1x dmb ishst, 2xdmb ish

    • c2版本包含 1x dmb ishst, 1x dmb ish, 1xstlr



  • runVolT3(读取 volatile v1):

    • c1版本 1x dmb ish, 1xdmb ishld

    • c2 版本 1x ldar



  • runVolT4(读取非易失性v2):没有内存障碍

如您所见,runVolT4(在不安全的重新发布后读取对象)不包含内存障碍。

这是否意味着线程可以将对象状态视为半初始化?

事实证明不是,在 ARMv8 上它仍然是安全的。

为什么?在代码中

查看return s.v2.f1;。这里 CPU 执行 2 次内存读取:


  • 首先它读取s.v2,其中包含对象的内存地址o

  • 然后它读取值o.f1from (memory address of o) + (offset of field f1inside Obj1)

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

在 ARMv8 上,这种地址依赖会阻止这两个读取的重新排序(请参见建模 ARMv8 架构中的MP+dmb.sy+addr示例,操作上:并发和 ISA,您可以在ARM 的内存模型工具中自行尝试)——因此我们可以保证看到v2完全初始化。

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

更重要的是,今天事实证明,OpenJDK 架构支持的所有地址依赖项都阻止了读取的重新排序(请参阅wiki中此表中的“依赖负载可以重新排序”或JSR-133中的表中的“数据依赖项顺序加载?”编译器编写者手册)。

因此,今天在 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 3runVolT4上面的相同:它不包含内存屏障指令。因此,Thread 3可以很容易地看到Thread 1乱序写入。

通常,在这种情况下不安全的重新发布今天在 OpenJDK 上很可能是不安全的。






推荐阅读
  • 使用 Azure Service Principal 和 Microsoft Graph API 获取 AAD 用户列表
    本文介绍了一段通用代码示例,该代码不仅能够操作 Azure Active Directory (AAD),还可以通过 Azure Service Principal 的授权访问和管理 Azure 订阅资源。Azure 的架构可以分为两个层级:AAD 和 Subscription。 ... [详细]
  • 本文详细介绍了如何构建一个高效的UI管理系统,集中处理UI页面的打开、关闭、层级管理和页面跳转等问题。通过UIManager统一管理外部切换逻辑,实现功能逻辑分散化和代码复用,支持多人协作开发。 ... [详细]
  • 本文详细介绍了 Apache Jena 库中的 Txn.executeWrite 方法,通过多个实际代码示例展示了其在不同场景下的应用,帮助开发者更好地理解和使用该方法。 ... [详细]
  • 本文详细介绍了Java中的访问器(getter)和修改器(setter),探讨了它们在保护数据完整性、增强代码可维护性方面的重要作用。通过具体示例,展示了如何正确使用这些方法来控制类属性的访问和更新。 ... [详细]
  • 实体映射最强工具类:MapStruct真香 ... [详细]
  • 本文将详细探讨 Java 中提供的不可变集合(如 `Collections.unmodifiableXXX`)和同步集合(如 `Collections.synchronizedXXX`)的实现原理及使用方法,帮助开发者更好地理解和应用这些工具。 ... [详细]
  • 深入解析Spring Cloud Ribbon负载均衡机制
    本文详细介绍了Spring Cloud中的Ribbon组件如何实现服务调用的负载均衡。通过分析其工作原理、源码结构及配置方式,帮助读者理解Ribbon在分布式系统中的重要作用。 ... [详细]
  • 在前两篇文章中,我们探讨了 ControllerDescriptor 和 ActionDescriptor 这两个描述对象,分别对应控制器和操作方法。本文将基于 MVC3 源码进一步分析 ParameterDescriptor,即用于描述 Action 方法参数的对象,并详细介绍其工作原理。 ... [详细]
  • 本文深入探讨了 Java 中的 Serializable 接口,解释了其实现机制、用途及注意事项,帮助开发者更好地理解和使用序列化功能。 ... [详细]
  • Android 渐变圆环加载控件实现
    本文介绍了如何在 Android 中创建一个自定义的渐变圆环加载控件,该控件已在多个知名应用中使用。我们将详细探讨其工作原理和实现方法。 ... [详细]
  • 本文探讨了如何在给定整数N的情况下,找到两个不同的整数a和b,使得它们的和最大,并且满足特定的数学条件。 ... [详细]
  • 本文详细介绍了Java中org.w3c.dom.Text类的splitText()方法,通过多个代码示例展示了其实际应用。该方法用于将文本节点在指定位置拆分为两个节点,并保持在文档树中。 ... [详细]
  • 毕业设计:基于机器学习与深度学习的垃圾邮件(短信)分类算法实现
    本文详细介绍了如何使用机器学习和深度学习技术对垃圾邮件和短信进行分类。内容涵盖从数据集介绍、预处理、特征提取到模型训练与评估的完整流程,并提供了具体的代码示例和实验结果。 ... [详细]
  • 在多线程编程环境中,线程之间共享全局变量可能导致数据竞争和不一致性。为了解决这一问题,Linux提供了线程局部存储(TLS),使每个线程可以拥有独立的变量副本,确保线程间的数据隔离与安全。 ... [详细]
  • 优化局域网SSH连接延迟问题的解决方案
    本文介绍了解决局域网内SSH连接到服务器时出现长时间等待问题的方法。通过调整配置和优化网络设置,可以显著缩短SSH连接的时间。 ... [详细]
author-avatar
夜阑人静1314coolgirl
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有