注意:这个问题与 volatile、AtomicLong 或所描述的用例中任何明显的缺陷无关。
我试图证明或排除的性质如下:
鉴于以下情况:
- 最近的 64 位 OpenJDK 7/8(最好是 7,但 8 也有帮助)
- 基于 Intel 的多处理系统
- 非易失性长原始变量
- 多个不同步的变异线程
- 不同步的观察者线程
观察者是否总是能保证遇到由修改器线程写入的完整值,或者单词撕裂是否存在危险?
JLS:尚无定论
此属性存在于 32 位基元和 64 位对象引用中,但 JLS 不保证长整型和双精度:
17.7。双精度和长精度的非原子处理: http://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.7
出于 Java 编程语言内存模型的目的,对非易失性 long 或 double 值的单次写入被视为两次单独的写入:每个 32 位一半写入一次。这可能会导致线程从一次写入中看到 64 位值的前 32 位,而从另一次写入中看到第二个 32 位。
但请稳住你的马:
[...]为了提高效率,这种行为是特定于实现的; Java 虚拟机的实现可以自由地以原子方式或分两部分执行对 long 和 double 值的写入。鼓励 Java 虚拟机的实现尽可能避免拆分 64 位值。 [...]
所以,JLSallows用于分割 64 位写入的 JVM 实现,以及鼓励开发商也相应调整鼓励JVM 实现者坚持使用 64 位写入。我们还没有最新版本的 HotSpot 的答案。
HotSpot JIT:谨慎乐观
由于单词撕裂最有可能发生在紧密循环和其他热点的范围内,因此我尝试分析 JIT 编译的实际程序集输出。长话短说:需要进一步测试,但我只能看到长整型上的原子 64 位操作。
I used hdis https://github.com/drazzib/openjdk-hsdis,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}
有趣的行标有“⇒”。
如您所见,加法运算是使用 64 位寄存器在四字(64 位)上执行的(rsi http://en.wikipedia.org/wiki/RSI_register#64-bit).
我还尝试通过在“长计数器”之前添加字节类型的填充变量来查看字节对齐是否是一个问题。汇编输出的唯一区别是:
before
0x00007fdf6106180c: movabs r10,0x7d6655660 ; {oop(a 'java/lang/Class' = 'Counter')}
after
0x00007fdf6106180c: movabs r10,0x7d6655668 ; {oop(a 'java/lang/Class' = 'Counter')}
两个地址都是 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
[...]
...我们将对第 7 行的“ladd”字节码指令感兴趣。
然而,我一直无法到目前为止特定于平台的实现。
感谢您的帮助!