非原子处理双长

问题描述

Java Language Specification 声明仅 'write' 操作被视为两部分:

就 Java 编程语言内存模型而言,对非易失性 long 或 double 值的单次写入被视为两次单独的写入:每个 32 位一半。>

但书 Java Concurrency In Practice 指出 'read' 或 'write' 操作被视为两部分:

JVM 被允许将 64 位读或写视为两个独立的 32 位操作。

哪个是准确的?

解决方法

深入研究 JVM 规范的过去版本,Java SE 6 中有一个 interesting section

如果 doublelong 变量未声明为 volatile,则为了加载、存储、读取和写入操作的目的,它被视为好像它是两个 32 位的变量;只要规则需要这些操作之一,就会执行两个这样的操作,每个 32 位一半执行一个。 Java 语言规范未定义 doublelong 变量的 64 位编码为两个 32 位数量的方式以及对变量一半的操作顺序。

这很重要,因为对 doublelong 变量的读或写可能会被实际主内存处理为两个 32 位读或写操作可能会在时间上分开,其他操作会在它们之间进行。因此,如果两个线程同时将不同的值分配给同一个共享的非 volatile doublelong 变量,该变量的后续使用可能会获得不等于其中任何一个的值分配的值,而是两个值的一些依赖于实现的混合。

一个实现可以自由地将 doublelong 值的加载、存储、读取和写入操作实现为原子 64 位操作;事实上,这是强烈鼓励的。为了当前流行的微处理器无法在 64 位数量上提供有效的原子内存事务,该模型将它们分成 32 位的一半。对于 Java 虚拟机来说,将单个变量上的所有内存事务定义为原子的会更简单;这个更复杂的定义是对当前硬件实践的务实让步。将来,这种让步可能会被取消。同时,提醒程序员明确同步对共享 doublelong 变量的访问。

此部分在以后的版本中不存在。我不想推测原因(尽管值得注意的是本节改编自 JLS 的第一版,而在 Java 5 中重新审视了内存模型),但这与 JCIP 中的描述相符。>

,

它既是读取又是写入。否则不能。想一想,如果您使用 ARM32 bits 上编码(例如电话)会怎样。当线程写入 long 时,读取器(除非您将 long 标记为 volatile)允许查看撕裂值。发生这种情况的原因很简单,因为架构没有 64 位寄存器来自动更新值。

当然,在 x86 上很容易实现,因为它有 64 bit 寄存器。但即使在那里,一个值也可以在一个缓存线上“用一只脚”,而在另一个缓存线上“用另一只脚”。因此,当有人更新第一个缓存行(而不是第二个)时,您可能会感到惊讶。这样的 VM 始终将 64 bit 值对齐到 2 的幂次偏移(以及 8 的倍数)。例如 class 喜欢:

static class Example {
    long x = 42;
}

将对齐为:

 Layout$Example object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                              
     0    12        (object header)                           
    12     4        (alignment/padding gap)                  
    16     8        long Example.x                                 

即:标题和 4 bytes 的实际值之间存在 x 间隙(填充),以便对齐。这可确保 x 不会出现在两个缓存行上。