是不稳定的?

Dan*_*iel 107 java concurrency memory-management volatile

在阅读JSR-133 Cookbook for Compiler Writers关于volatile的实现之后,特别是"与原子指令的交互"部分我认为读取volatile变量而不更新它需要LoadLoad或LoadStore屏障.在页面的下方,我看到LoadLoad和LoadStore在X86 CPU上实际上是无操作的.这是否意味着可以在x86上没有显式缓存失效的情况下完成易失性读取操作,并且正常变量读取速度快(忽略volatile的重新排序约束)?

我相信我不明白这一点.有人可以照顾我吗?

编辑:我想知道多处理器环境是否存在差异.在单CPU系统上,CPU可能会查看它自己的线程缓存,正如John V.所述,但在多CPU系统上,CPU必须有一些配置选项,这是不够的,主内存必须被击中,使得波动较慢在多CP​​U系统上,对吗?

PS:在我学习更多相关信息的路上,我偶然发现了以下很棒的文章,因为这个问题可能对其他人很有意思,我会在这里分享我的链接:

Mic*_*ker 119

在英特尔,无争用的易失性读取非常便宜.如果我们考虑以下简单情况:

public static long l;

public static void run() {        
    if (l == -1)
        System.exit(-1);

    if (l == -2)
        System.exit(-1);
}
Run Code Online (Sandbox Code Playgroud)

使用Java 7打印汇编代码的能力,run方法看起来像:

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb396ce80: mov    %eax,-0x3000(%esp)
0xb396ce87: push   %ebp
0xb396ce88: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 33)
0xb396ce8e: mov    $0xffffffff,%ecx
0xb396ce93: mov    $0xffffffff,%ebx
0xb396ce98: mov    $0x6fa2b2f0,%esi   ;   {oop('Test2')}
0xb396ce9d: mov    0x150(%esi),%ebp
0xb396cea3: mov    0x154(%esi),%edi   ;*getstatic l
                                    ; - Test2::run@0 (line 33)
0xb396cea9: cmp    %ecx,%ebp
0xb396ceab: jne    0xb396ceaf
0xb396cead: cmp    %ebx,%edi
0xb396ceaf: je     0xb396cece         ;*getstatic l
                                    ; - Test2::run@14 (line 37)
0xb396ceb1: mov    $0xfffffffe,%ecx
0xb396ceb6: mov    $0xffffffff,%ebx
0xb396cebb: cmp    %ecx,%ebp
0xb396cebd: jne    0xb396cec1
0xb396cebf: cmp    %ebx,%edi
0xb396cec1: je     0xb396ceeb         ;*return
                                    ; - Test2::run@28 (line 40)
0xb396cec3: add    $0x8,%esp
0xb396cec6: pop    %ebp
0xb396cec7: test   %eax,0xb7732000    ;   {poll_return}
;... lines removed
Run Code Online (Sandbox Code Playgroud)

如果你看看2个对getstatic的引用,第一个涉及从内存加载,第二个跳过加载,因为值已经从它已加载到的寄存器中重用(长是64位,在我的32位笔记本电脑上)它使用2个寄存器).

如果我们将l变量设为volatile,则生成的程序集会有所不同.

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb3ab9340: mov    %eax,-0x3000(%esp)
0xb3ab9347: push   %ebp
0xb3ab9348: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 32)
0xb3ab934e: mov    $0xffffffff,%ecx
0xb3ab9353: mov    $0xffffffff,%ebx
0xb3ab9358: mov    $0x150,%ebp
0xb3ab935d: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab9365: movd   %xmm0,%eax
0xb3ab9369: psrlq  $0x20,%xmm0
0xb3ab936e: movd   %xmm0,%edx         ;*getstatic l
                                    ; - Test2::run@0 (line 32)
0xb3ab9372: cmp    %ecx,%eax
0xb3ab9374: jne    0xb3ab9378
0xb3ab9376: cmp    %ebx,%edx
0xb3ab9378: je     0xb3ab93ac
0xb3ab937a: mov    $0xfffffffe,%ecx
0xb3ab937f: mov    $0xffffffff,%ebx
0xb3ab9384: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab938c: movd   %xmm0,%ebp
0xb3ab9390: psrlq  $0x20,%xmm0
0xb3ab9395: movd   %xmm0,%edi         ;*getstatic l
                                    ; - Test2::run@14 (line 36)
0xb3ab9399: cmp    %ecx,%ebp
0xb3ab939b: jne    0xb3ab939f
0xb3ab939d: cmp    %ebx,%edi
0xb3ab939f: je     0xb3ab93ba         ;*return
;... lines removed
Run Code Online (Sandbox Code Playgroud)

在这种情况下,对变量l的两个getstatic引用都涉及来自内存的加载,即该值不能保存在多个volatile读取的寄存器中.为了确保存在原子读取,将值从主存储器读取到MMX寄存器中,movsd 0x6fb7b2f0(%ebp),%xmm0使读取操作成为单个指令(从前面的示例中我们看到64位值通常需要在32位系统上进行两次32位读取).

因此,易失性读取的总体成本将大致相当于内存负载,并且可以像L1缓存访问一样便宜.但是,如果另一个核心正在写入volatile变量,则缓存行将无效,需要主存储器或L3缓存访问.实际成本在很大程度上取决于CPU架构.即使在Intel和AMD之间,缓存一致性协议也是不同的.


Joh*_*int 20

一般而言,在大多数现代处理器上,易失性负载与正常负载相当.易失性存储大约是montior-enter/monitor-exit时间的1/3.这可以在缓存一致的系统上看到.

为了回答OP的问题,易失性写入是昂贵的,而读取通常不是.

这是否意味着可以在x86上没有显式高速缓存失效的情况下完成易失性读取操作,并且是快速正常变量读取(忽略易失性的重新排序约束)?

是的,有时在验证字段时,CPU可能甚至不能访问主内存,而是监视其他线程缓存并从那里获取值(非常一般性的解释).

但是,我再次提出Neil的建议,如果你有一个由多个线程访问的字段,你将它包装为AtomicReference.作为AtomicReference,它执行大致相同的读/写吞吐量,但更明显的是该字段将被多个线程访问和修改.

编辑以回答OP的编辑:

缓存一致性是一个复杂的协议,但简而言之:CPU将共享一个连接到主存储器的公共缓存行.如果CPU加载内存而没有其他CPU拥有它,CPU将认为它是最新的值.如果另一个CPU尝试加载相同的内存位置,则已加载的CPU将意识到这一点并实际共享对请求CPU的高速缓存引用 - 现在请求CPU在其CPU高速缓存中具有该内存的副本.(它永远不必在主内存中查找参考)

涉及的协议相当多,但这可以了解正在发生的事情.另外,为了回答您的其他问题,在没有多个处理器的情况下,易失性读/写实际上可以比使用多个处理器更快.实际上,有些应用程序可以在单个CPU和多个CPU的同时运行得更快.

  • AtomicReference只是volatile字段的包装,添加了本机函数,提供了getAndSet,compareAndSet等附加功能,因此从性能的角度来看,使用它只需要增加的功能就很有用.但我想知道为什么你在这里提到操作系统?该功能直接在CPU操作码中实现.这是否意味着在多处理器系统中,一个CPU不了解其他CPU的缓存内容,因为CPU总是需要访问主内存,因此挥发性较慢? (5认同)

Nei*_*ett 11

用Java Memory Model(JSR 133中为Java 5+定义)来说,对volatile变量的任何操作 - 读或写 - 都会在同一个变量上创建一个先发生关系.这意味着强制编译器和JIT避免某些优化,例如重新排序线程内的指令或仅在本地缓存中执行操作.

由于某些优化不可用,因此产生的代码必然会慢一些,尽管可能不是很多.

然而,volatile除非您知道它将从synchronized块之外的多个线程访问,否则您不应该创建变量.即便如此,你应该考虑volatile是否是最好的选择synchronized,AtomicReference以及它的朋友,明确的Lock类等.