编辑:我想知道在多处理器环境中是否存在差异。在单CPU系统上,CPU可能会查看自己的线程缓存,如John V.所述,但在多CPU系统上,必须为CPU提供一些配置选项,以使这不足够,并且必须访问主内存,从而使volatile在多CPU系统上变慢,对吗?
PS:在学习更多关于此事时,我偶然发现了以下优秀的文章,由于这个问题可能对其他人有用,因此在此分享我的链接:
在Intel上,未受争议的易失性读取操作相当便宜。如果我们考虑以下简单情况:
public static long l;
public static void run() {
if (l == -1)
System.exit(-1);
if (l == -2)
System.exit(-1);
}
使用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
如果您查看对getstatic的2个引用,第一个涉及从内存中加载,而第二个跳过了加载,因为该值是从已经加载到的寄存器(long为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
在这种情况下,变量 l 的两个 getstatic 引用都涉及从内存加载,即该值无法在多个 volatile 读取之间保留在寄存器中。为确保存在原子读取,该值从主内存读取到 MMX 寄存器中 movsd 0x6fb7b2f0(%ebp),%xmm0
,使读取操作成为单个指令(从前面的示例中我们看到,64位值通常需要在32位系统上进行两个32位读取)。一般来说,大多数现代处理器上,volatile load(即volatile读取)与普通load(即普通读取)相似。而volatile store(即volatile写入)则约为montior-enter/monitor-exit(即进入/退出监视器)的1/3。这种情况在具有缓存一致性的系统中可见。
回答楼主的问题,volatile写入通常是昂贵的,而读取通常不是。
这是否意味着在x86上,可以进行没有显式缓存失效的volatile read操作,并且与普通变量读取一样快(不考虑volatile的重新排序限制)?
是的,有时验证字段时,CPU可能甚至不会触及主内存,而是窃听其他线程的缓存并从那里获取值(非常一般的解释)。
但是,我赞成Neil的建议,如果您有一个被多个线程访问的字段,则应将其封装为AtomicReference。作为AtomicReference,它的读/写吞吐量大致相同,但还更明显地表明该字段将被多个线程访问和修改。
编辑以回答楼主的编辑:
缓存一致性是一种有些复杂的协议,但简单来说:CPU将共享连接到主内存的公共高速缓存行。如果一台CPU加载了内存,并且没有其他CPU使用该内存,那么该CPU将假定它是最新的值。如果另一台CPU尝试加载相同的内存位置,则已经加载的CPU将意识到这一点,并实际上共享对请求CPU的缓存引用 - 现在请求CPU在其CPU缓存中具有该内存的副本。(它从未必须在主内存中查找引用)
还涉及相当多的协议,但这给出了正在发生的情况的概念。此外,回答您的另一个问题,在没有多个处理器的情况下,volatile读/写实际上可以比具有多个处理器更快。有些应用程序在单个CPU并发运行时甚至会运行得更快。
stop
布尔值,只需更改一次即可。 - bestsssvolatile
变量的任何操作 - 读取或写入 - 都会与同一变量的任何其他操作创建一个 happens-before 关系。这意味着编译器和JIT被迫避免某些优化,例如在线程内重新排序指令或仅在本地缓存中执行操作。synchronized
块之外的多个线程访问变量,否则不应使变量成为volatile
。即使如此,您也应该考虑synchronized
、AtomicReference
及其伙伴、显式Lock
类等是否比volatile
更好选择。访问一个volatile变量在很多方面类似于在同步块中封装对普通变量的访问。例如,访问volatile变量可以防止CPU在访问之前和之后重新排序指令,这通常会降低执行速度(虽然我无法确定降低了多少速度)。
更普遍地说,在多处理器系统上,我不认为访问volatile变量可以没有代价--必须有一种方法来确保在处理器A上的写操作将与处理器B上的读操作同步。