注意:此问题与volatile,AtomicLong或所述用例中的任何明显缺陷无关。
鉴于以下内容:
- 最近的64位OpenJDK 7/8(最好7位,但8位也很有帮助)
- 基于Intel的多处理系统
- 非易失性长原始变量
- 多个未同步的mutator线程
- 一个不同步的观察者线程
观察者是否始终保证会遇到由mutator线程写的完整值,或者是否有撕裂危险的单词?
此属性对于32位基元和64位对象引用是存在的,但是对于long和double,JLS不保证:
17.7. Non-atomic Treatment of double and long:
出于Java编程语言内存模型的目的,对非易失性long或double值的单次写入被视为两个单独的写入:每个32位半写一次。这可能导致线程从一次写入看到64位值的前32位,而从另一次写入看到第二次32位。
但是抱着你的马:
[...]为了效率,这种行为是特定于实现的; Java虚拟机的实现可以自由地以原子方式或分两部分执行对long和double值的写入。鼓励Java虚拟机的实现避免在可能的情况下拆分64位值。 [...]
因此,JLS 允许 JVM实现拆分64位写入,鼓励开发人员进行相应调整,但鼓励 JVM实现者坚持使用64位写入。我们还没有得到HotSpot最新版本的答案。
由于单词撕裂最有可能发生在紧密循环和其他热点的范围内,我试图分析JIT编译的实际汇编输出。长话短说:需要进一步测试,但我只能在longs上看到原子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;
}
}
我确保始终使用大于MAX_INT(1e12到1e12 + 1e5)的值,并重复操作足够的时间(1e5)以触发JIT。
编译完成后,我用hdis执行了Counter.main(),如下所示:
java -XX:+UnlockDiagnosticVMOptions \
-XX:PrintAssemblyOptions=intel \
-XX:CompileCommand=print,Counter.put \
Counter
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}
有趣的行标有&#39;⇒&#39;。 如您所见,使用64位寄存器(rsi)在四字(64位)上执行添加操作。
我还试图通过在“长计数器”之前添加一个字节类型的填充变量来查看字节对齐是否存在问题。装配输出的唯一区别是:
之前的
0x00007fdf6106180c: movabs r10,0x7d6655660 ; {oop(a 'java/lang/Class' = 'Counter')}
后的
0x00007fdf6106180c: movabs r10,0x7d6655668 ; {oop(a 'java/lang/Class' = 'Counter')}
这两个地址都是64位对齐的,而且这些地址是r10,...&#39;调用使用的是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
[...]
...我们会对&#39; ladd&#39;感兴趣。第7行的字节码指令。 但是,到目前为止,我还无法trace it through进行特定于平台的实施。
您的帮助表示赞赏!
答案 0 :(得分:4)
事实上,您已经回答了自己的问题。
没有&#34;非原子处理&#34; 64位HotSpot JVM上的double
和long
,因为
答案 1 :(得分:2)
VNA05-J。在读取和写入64位值时确保原子性
...
VNA05-EX1:对于保证这一规则的平台,可以忽略此规则 64位长和双值作为原子读取和写入 操作。但请注意,此类保证不可移植 跨越不同的平台。
上面的链接在安全性的上下文中讨论了这个问题,并且似乎暗示在64位平台上你确实可以假设长的赋值是原子的。 32位系统在服务器环境中变得越来越少,所以它并不是一个奇怪的假设。请注意,异常在哪些平台上做出这种保证有点模糊,并且没有明确说明64位英特尔上的64位openjdk就好了。