在Java中,当我们有两个线程共享以下变量时:
int a;
volatile int b;
如果线程1做到了:
a = 5;
b = 6;
然后在这两个指令之间插入一个StoreStore屏障,并且将" a"刷新回主存储器。
现在,如果线程2做到了:
if(b == 6)
a++;
在两者之间插入了LoadLoad障碍,我们保证,如果新值" b"可见,那么新值" a"也可见。 但是实际上是如何实现的呢? LoadLoad是否会使CPU缓存/寄存器无效? 还是只是指示CPU重新从CPU中读取从volatile中读取的变量的值?
我已经找到有关LoadLoad barrier的以下信息(http://gee.cs.oswego.edu/dl/jmm/cookbook.html):
LoadLoad Barriers The sequence: Load1; LoadLoad; Load2 ensures that
Load1's data are loaded before data accessed by Load2 and all
subsequent load instructions are loaded. In general, explicit LoadLoad
barriers are needed on processors that perform speculative loads
and/or out-of-order processing in which waiting load instructions can
bypass waiting stores. On processors that guarantee to always preserve
load ordering, the barriers amount to no-ops.
但这并不能真正解释这是如何实现的。
答案取决于处理器体系结构-同一文档中有一个表,其中每个处理器指令均显示LoadLoad是x86上的空操作。
那它怎么运作的呢?我的意思是,在StoreStore之后,值将刷新回内存中。但是,线程2应该如何看待它们呢?如果该LoadLoad评估为no-op,则线程2可以继续使用缓存的值。
因为处理器的内存模型足够强大,因此可以保证确实如此。林试图说的是,Java承诺,如果您使用volatile,将/将不会发生某些事情。如何在JVM中实现此功能是特定于处理器的,并且使用临时指令(如果相关,则不使用任何指令)。您可以在此处了解有关LoadLoad / x86点的更多信息:altair.cs.oswego.edu/pipermail/concurrency-interest/2012-July/
您可以阅读有关LoadLoad并非空当的CPU体系结构(例如ARM)的更多信息。似乎是沉重的东西:)
我将举例说明如何实现这一目标。您可以在此处阅读更多详细信息。如前所述,对于x86处理器,LoadLoad最终变为无操作。在我链接的文章中,马克指出
Doug lists the StoreStore, LoadLoad and LoadStore
因此,本质上,唯一需要的障碍是用于x86体系结构的StoreLoad。那么如何从低水平实现呢?
这是博客的摘录:
这是它为易失性和非易失性读取生成的代码:
nop ;*synchronization entry
mov 0x10(%rsi),%rax ;*getfield x
对于易失性写入:
xchg %ax,%ax
movq $0xab,0x10(%rbx)
lock addl $0x0,(%rsp) ;*putfield x
lock指令是Doug的食谱中列出的StoreLoad。但是,锁定指令还会将所有读取与其他进程同步,如所列
Locked instructions can be used to synchronize data written by one
processor and read by another processor.
这减少了必须为易失性负载发出LoadLoad LoadStore屏障的开销。
话虽如此,我将重申亚述指出的内容。对于开发人员来说,发生的方式并不重要(如果您对处理器/编译器实现程序感兴趣的话,则另当别论)。 volatile关键字是一种界面说法
您将获得由另一个线程编写的最新读物
您将不会因JIT编译器优化而烦恼。
好的链接。我会悄悄地改写一下:"您将获得由另一个线程编写的最新读取信息" =>"随后从易失性变量读取内容时,您最终将看到写入内容-"最终"实际上几乎立即就意味着含义";- )
实际上,重要的是,如果您对应用程序运行哪种硬件最快或如何获得最高性能感兴趣。我们希望从四插槽Xeon(64 SMT)中受益,远比我们最终受益。如果您无法控制硬件或仅在单个套接字上运行,那可能就不成问题了,但是并发实现的详细信息以及它们如何影响大型计算机的可伸缩性,如果早日知道,肯定会影响设计。
@RalfH我使用volatile关键字代表普通开发人员发言。一般来说,开发人员无需担心易失性的实现方式。当您知道基础架构改变了您如何以不同方式使用volatile时,您是否看到过实例?
:)不,因为我也不知道硬件的工作方式,所以我宁愿进行性能分析。这就是我注意到类似ConcurrentHashMap的volatile获取性能不好的原因,因此,一旦它们的内容稳定,我们最终将它们替换为CopyOnWriteHashMaps。如果您想知道即将发生的事情,那么硬件的详细信息就在测试中。就像我想知道的那样,Haswell中的硬件事务存储对Java并发意味着什么。
好吧,那令人费解:)我不确定我的理解是否正确:由于x86上的LoadLoad障碍是无操作的,因此Marcs程序生成了StoreLoad障碍? (以" lock"开头的行)。如果易失性读产生了一条额外的指令(锁),为什么他说我们可以期望易失性读是免费的?回到我最初的问题,如果我们在读完一个volatile之后在Marcs示例中添加了一个对非易失性的读取,那么"锁定"指令是否会发挥作用(确保非易失性的可见性)?
@Janek如果您读得更多,他会提到It appears as though reads to volatile variables are not free in Java on x86, or at least on the tested setup.并且StoreLoad是通过写入易失性变量而不是读取生成的。与非易失性负载相比,非易失性负载增加了25%,这是因为x86缓存的一致性。
@Jarek回答您的其他问题。 (ensure visibility of a non-volatile)仅当您对volatile字段进行写操作时。否则就不会发生事前关系,因此您无需担心可见性。如果确实这样做了,则lock指令似乎可以保证可见性。我正在基于data written by one processor and read by another processor做出该断言
"我写了一个很小的玩具程序,它循环访问了一个volatile变量"-认为访问意味着读。可惜他没有附上消息来源。因此,最重要的是,在写入volatile时插入的StoreLoad屏障完成了其他处理器在该写入之前查看内存更改所需的所有工作。然后,易失性读取与非易失性读取完全相同(在asm指令方面)。为了弄清楚这个StoreLoad屏障如何将内存更改推向其他处理器,我应该研究锁指令,也许还要研究xchg?
lock instruction and maybe xchg是,还有缓存一致性协议:en.wikipedia.org/wiki/Cache_coherence
If that LoadLoad evaluates to no-op then thread 2 can continue using cached values.
食谱中的"可以订购"表对此进行了说明。
编程顺序为
read b
read a
write a
"缓存一个",表示代码已重新排序
read a
...
read b
禁止重新排序。
我实际上是指在CPU缓存中进行真正的缓存,而不是对指令进行重新排序。
负载是负载,它将不会从CPU寄存器中读取。
@zhong我认为Janeks担心的是,如果一个域在另一个处理器写操作后从未初始化加载,那么该域如何在处理器寄存器中保持最新状态。