nad*_*vwr 9 java concurrency jvm jvm-hotspot java-memory-model
注意:此问题与volatile,AtomicLong或所描述的用例中的任何感知缺陷无关.
鉴于以下内容:
- 最近的64位OpenJDK 7/8(最好7位,但8位也很有帮助)
- 多处理英特尔基础系统
- 非易失性长原始变量
- 多个不同步的mutator线程
- 一个不同步的观察者线程
观察者是否始终保证会遇到由变异线程写的完整值,或者是撕裂危险的单词?
此属性对于32位基元和64位对象引用是存在的,但是对于long和double,JLS不保证:
17.7.非原子对double和long的处理:
出于Java编程语言内存模型的目的,对非易失性long或double值的单次写入被视为两个单独的写入:每个32位一半写入一次.这可能导致线程从一次写入看到64位值的前32位,而从另一次写入看到第二次32位的情况.
但是抱着你的马:
[...]为了效率,这种行为是特定于实现的; Java虚拟机的实现可以自由地以原子方式或分两部分执行对long和double值的写入.鼓励Java虚拟机的实现避免在可能的情况下拆分64位值.[...]
因此,JLS 允许 JVM实现拆分64位写入,并鼓励开发人员相应地进行调整,但也鼓励 JVM实现者坚持使用64位写入.我们还没有回答最新版本的HotSpot.
由于单词撕裂最有可能发生在紧密循环和其他热点的范围内,我试图分析JIT编译的实际汇编输出.长话短说:需要进一步测试,但我只能在long上看到原子64位操作.
我使用了hdis,一个OpenJDK的反汇编插件.在我老化的OpenJDK 7u25版本中构建并安装了插件之后,我开始编写一个简短的程序:
public class Counter {
static long counter = 0;
public static void main(String[] _) {
for (long i = (long)1e12; i < (long)1e12 + 1e5; i++)
put(i);
System.out.println(counter);
}
static void put(long v) {
counter += v;
}
}
Run Code Online (Sandbox Code Playgroud)
我确保始终使用大于MAX_INT(1e12到1e12 + 1e5)的值,并重复操作足够的次数(1e5)以触发JIT.
编译后,我用hdis执行Counter.main(),如下所示:
java -XX:+UnlockDiagnosticVMOptions \
-XX:PrintAssemblyOptions=intel \
-XX:CompileCommand=print,Counter.put \
Counter
Run Code Online (Sandbox Code Playgroud)
由JIT为Counter.put()生成的程序集如下(为方便起见,添加了十进制行数):
01 # {method} 'put' '(J)V' in 'Counter'
02 ? # parm0: rsi:rsi = long
03 # [sp+0x20] (sp of caller)
04 0x00007fdf61061800: sub rsp,0x18
05 0x00007fdf61061807: mov QWORD PTR [rsp+0x10],rbp ;*synchronization entry
06 ; - Counter::put@-1 (line 15)
07 0x00007fdf6106180c: movabs r10,0x7d6655660 ; {oop(a 'java/lang/Class' = 'Counter')}
08 ? 0x00007fdf61061816: add QWORD PTR [r10+0x70],rsi ;*putstatic counter
09 ; - Counter::put@5 (line 15)
10 0x00007fdf6106181a: add rsp,0x10
11 0x00007fdf6106181e: pop rbp
12 0x00007fdf6106181f: test DWORD PTR [rip+0xbc297db],eax # 0x00007fdf6cc8b000
13 ; {poll_return}
Run Code Online (Sandbox Code Playgroud)
有趣的线条标有'⇒'.如您所见,使用64位寄存器(rsi)在四字(64位)上执行添加操作.
我还试图通过在'long counter'之前添加一个字节类型的填充变量来查看字节对齐是否存在问题.装配输出的唯一区别是:
之前
0x00007fdf6106180c: movabs r10,0x7d6655660 ; {oop(a 'java/lang/Class' = 'Counter')}
Run Code Online (Sandbox Code Playgroud)
后
0x00007fdf6106180c: movabs r10,0x7d6655668 ; {oop(a 'java/lang/Class' = 'Counter')}
Run Code Online (Sandbox Code Playgroud)
两个地址都是64位对齐的,而'movabs r10,...'调用使用的是64位寄存器.
到目前为止,我只测试过添加.我假设减法行为类似.
其他操作,如按位操作,赋值,乘法等仍有待测试(或由熟悉HotSpot内部的人确认).
这让我们得到了非JIT场景.让我们反编译Compiler.class:
$ javap -c Counter
[...]
static void put(long);
Code:
0: getstatic #8 // Field counter:J
3: lload_0
4: ladd
5: putstatic #8 // Field counter:J
8: return
[...]
Run Code Online (Sandbox Code Playgroud)
...我们将对第7行的'ladd'字节码指令感兴趣.但是,到目前为止,我还无法将其追溯到特定于平台的实现.
感谢您的帮助!
事实上,你已经回答了你自己的问题。
有没有“非原子治疗”double和long在64位的HotSpot JVM,因为
VNA05-J。确保读写 64 位值时的原子性
....
VNA05-EX1:对于保证 64 位 long 和 double 值作为原子操作进行读写的平台,可以忽略此规则。但请注意,此类保证不可跨不同平台移植。
上面的链接在安全性的背景下讨论了这个问题,似乎表明在 64 位平台上您确实可以假设长赋值是原子的。32 位系统在服务器环境中变得越来越少见,因此做出这样的假设并不奇怪。请注意,该例外对于哪些平台做出此保证有点模糊,并且没有明确声明 64 位 intel 上的 64 位 openjdk 没问题。