64位OpenJDK 7/8中并发长写的值完整性保证

时间:2014-08-07 02:21:07

标签: java concurrency jvm jvm-hotspot java-memory-model

注意:此问题与volatile,AtomicLong或所述用例中的任何明显缺陷无关。

我试图证明或排除的属性如下:

  

鉴于以下内容:

     
      
  • 最近的64位OpenJDK 7/8(最好7位,但8位也很有帮助)
  •   
  • 基于Intel的多处理系统
  •   
  • 非易失性长原始变量
  •   
  • 多个未同步的mutator线程
  •   
  • 一个不同步的观察者线程
  •   
     

观察者是否始终保证会遇到由mutator线程写的完整值,或者是否有撕裂危险的单词?

JLS:不确定

此属性对于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最新版本的答案。

HotSpot JIT:谨慎乐观

由于单词撕裂最有可能发生在紧密循环和其他热点的范围内,我试图分析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进行特定于平台的实施。

您的帮助表示赞赏!

2 个答案:

答案 0 :(得分:4)

事实上,您已经回答了自己的问题。

没有&#34;非原子处理&#34; 64位HotSpot JVM上的doublelong ,因为

  1. HotSpot使用64位寄存器存储64位值(x86_64.adx86_32.ad)。
  2. HotSpot对齐64位边界(universe.inline.hpp
  3. 上的64位字段

答案 1 :(得分:2)

https://www.securecoding.cert.org/confluence/display/java/VNA05-J.+Ensure+atomicity+when+reading+and+writing+64-bit+values

  

VNA05-J。在读取和写入64位值时确保原子性

     

...

     

VNA05-EX1:对于保证这一规则的平台,可以忽略此规则   64位长和双值作为原子读取和写入   操作。但请注意,此类保证不可移植   跨越不同的平台。

上面的链接在安全性的上下文中讨论了这个问题,并且似乎暗示在64位平台上你确实可以假设长的赋值是原子的。 32位系统在服务器环境中变得越来越少,所以它并不是一个奇怪的假设。请注意,异常在哪些平台上做出这种保证有点模糊,并且没有明确说明64位英特尔上的64位openjdk就好了。