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

Java 64位OpenJDK 7/8中并发长写的值完整性保证,java,concurrency,jvm,jvm-hotspot,java-memory-model,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不保证此属性: 对

注意:这个问题与所描述用例中的volatile、AtomicLong或任何可感知的缺陷无关

我试图证明或排除的财产如下: 鉴于以下情况:

  • 最近的64位OpenJDK 7/8(最好是7,但8也很有用)
  • 基于Intel的多处理器系统
  • 非易失性长基元变量
  • 多个未同步的mutator线程
  • 未同步的观察者线程
观察者是否总是保证会遇到由mutator线程编写的完整值,或者单词撕裂是一种危险

JLS:不确定 此属性适用于32位原语和64位对象引用,但对于long和double,JLS不保证此属性:


对于Java编程语言内存模型而言,对非易失性长或双值的单次写入被视为两次单独的写入:每32位半写一次。这可能导致线程在一次写入中看到64位值的前32位,在另一次写入中看到第二个32位

但请不要着急:

[…]为了提高效率,这种行为是特定于实现的;Java虚拟机的实现可以自由地以原子方式或分两部分执行对长值和双值的写入。鼓励Java虚拟机的实现尽可能避免拆分64位值。[……]

因此,JLS允许JVM实现拆分64位写入,并鼓励开发人员进行相应调整,但也鼓励JVM实现人员坚持64位写入。对于HotSpot的最新版本,我们还没有答案

热点JIT:谨慎乐观 由于单词撕裂最有可能发生在紧循环和其他热点的范围内,因此我尝试分析JIT编译的实际程序集输出。长话短说:需要进一步的测试,但我只能在long上看到原子64位操作

我使用了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;
  }
}
JIT为Counter.put()生成的程序集如下所示(为方便起见添加了十进制行号):

有趣的线条用'⇒'. 如您所见,使用64位寄存器()在四字(64位)上执行添加操作

我还试图通过在“longcounter”之前添加一个字节类型的填充变量来查看字节对齐是否存在问题。装配输出的唯一区别是:

以前

之后

两个地址都是64位对齐的,那些“movabs r10,…”调用使用64位寄存器

到目前为止,我只测试了加法。我假设减法的行为类似。
其他操作,如按位操作、赋值、乘法等仍有待测试(或由足够熟悉热点内部结构的人员确认)

口译员:没有定论 这就给我们留下了非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”字节码指令感兴趣。 然而,到目前为止,我还无法实现特定于平台的实现

谢谢你的帮助

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

VNA05-EX1:对于保证 64位长和双精度值作为原子进行读写 操作。但是,请注意,此类担保是不可移植的 跨越不同的平台


上面的链接从安全性的角度讨论了这个问题,似乎表明在64位平台上,您确实可以假设长分配是原子的。32位系统在服务器环境中越来越少见,因此做出这样的假设并不奇怪。请注意,例外情况有点含糊不清,在哪些平台上可以保证这一点,例如,没有明确说明64位intel上的64位openjdk是可以的。

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

在64位热点JVM上没有对
double
long
进行“非原子处理”,因为

  • HotSpot使用64位寄存器存储64位值(vs.)
  • 热点在64位边界()上对齐64位字段

  • +考虑到编写与单词对齐的代码要容易得多,我认为在64位开放JDK JVM中,64位类型的读写很可能是原子的。然而,依赖这种行为似乎是愚蠢的,因为人们永远不知道最终希望Java代码在什么平台上运行。无论如何,编译器编写者在优化易失性64位类型时可能已经利用了这种行为,因此,如果不使用volatile关键字,您不会获得太多收益。我假设64位JDK是原子级的,32位分为两部分。您链接到的问题@Holger已经回答了这个问题,它提供了有价值的信息。我会查一下参考资料。谢谢
    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}
    
        0x00007fdf6106180c: movabs r10,0x7d6655660    ;   {oop(a 'java/lang/Class' = 'Counter')}
    
        0x00007fdf6106180c: movabs r10,0x7d6655668    ;   {oop(a 'java/lang/Class' = 'Counter')}
    
    $ 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
    [...]