重要的不是栅栏的运行时效果,而是强制编译器重新加载内容的编译时效果。
Your t1
循环不包含volatile
读取或任何其他可以与另一个线程同步的内容,因此不能保证它会ever注意任何变量的任何变化。即,当 JIT 到 asm 中时,编译器可以创建一个循环,将值加载到寄存器中一次,而不是每次都从内存中重新加载。这是您始终希望编译器能够对非共享数据进行的优化,这就是为什么该语言具有允许它在不可能同步时执行此操作的规则。
当然,条件可以被提升到循环之外。所以没有任何障碍或任何东西,您的阅读器循环可以 JIT 到实现此逻辑的 asm 中:
if(t.flag) {
for(;;){} // infinite loop
}
除了排序之外,Java 的其他部分volatile
是假设其他线程可能会异步更改它,因此不能假设多次读取给出相同的值。
But unsafe.loadFence();
使 JVM 重新加载t.flag
每次迭代都来自(缓存一致性)内存。我不知道这是否是 Java 规范所要求的,或者仅仅是使其能够工作的实现细节。
如果这是带有非atomic
变量(这在 C++ 中是未定义的行为),您会在 GCC 等编译器中看到完全相同的效果。_mm_lfence
也将是一个编译时完全障碍以及发出无用的lfence
指令,有效地告诉编译器所有内存可能已更改,因此需要重新加载。因此它无法重新排序其上的负载,或将它们提升出循环。
顺便说一句,我不太确定unsafe.loadFence()
甚至 JITlfence
x86 上的指令。它is对于内存排序来说毫无用处(除了非常模糊的东西,比如从 WC 内存中隔离 NT 加载,例如从视频 RAM 进行复制,JVM 可以假设这不会发生),因此 x86 的 JVM JITing 可以将其视为编译时障碍。就像 C++ 编译器所做的那样std::atomic_thread_fence(std::memory_order_acquire);
- 阻止编译时跨屏障重新排序负载,但不发出 asm 指令,因为运行 JVM 的主机的 asm 内存已经足够强大。
在线程 2 中,unsafe.fullFence();
我认为没用。它只是让that线程等待,直到早期的存储变得全局可见,然后才能发生任何后续的加载/存储。t.flag = false;
是一个明显的副作用,无法通过优化消除,因此无论后面是否有障碍,它都肯定会发生在 JITed asm 中,即使它不是volatile
。而且它不能被延迟或与其他东西合并,因为同一线程中没有其他东西。
Asm 存储始终对其他线程可见,唯一的问题是当前线程是否等待其存储缓冲区耗尽,然后再在此线程中执行更多操作(尤其是加载)。即阻止所有重新排序,包括 StoreLoad。爪哇volatile
这样做,就像 C++ 一样memory_order_seq_cst
(通过在每个存储之后使用完整的屏障),但如果没有屏障,它仍然是像 C++ 一样的存储memory_order_relaxed
。 (或者当 JITing x86 asm 时,加载/存储实际上与获取/释放一样强大。)
缓存是一致的,并且存储缓冲区总是尽可能快地耗尽自身(提交给 L1d 缓存),以便为更多存储的执行腾出空间。
警告:我对Java了解不多,而且我不知道分配一个非-到底有多么不安全/未定义。volatile
在一个线程中读取它并在另一个线程中读取它,而无需同步。根据您所看到的行为,这听起来与您在 C++ 中看到的与非非相同的事情完全相同atomic
变量(启用优化,就像 HotSpot 总是做的那样)
(根据 @Margaret 的评论,我更新了一些关于我假设 Java 同步如何工作的猜测。如果我有任何错误陈述,请编辑或评论。)
在非 C++ 数据竞争中atomic
变量总是未定义的行为,但是当然,当为真正的 ISA(不进行硬件竞争预防)进行编译时,结果有时是人们想要的。
PS: 仅仅使用屏障来强制编译器重新读取值通常并不安全:即使源将其复制到局部变量,它也可以选择多次重新读取该值。因此,同一个 tmp var 在一次执行中可能看起来既是 true 又是 false。至少在 C 和 C++ 中是这样,因为数据竞争在这些语言中是未定义的行为。看谁害怕一个糟糕的优化编译器?在 LWN 上,如果您只使用屏障和普通(非volatile
)变量。再说一次,我不知道这是否是 Java 中可能存在的问题,或者语言规范是否会禁止 JVM 在int tmp = shared_plain_int;
if tmp
在函数调用中多次使用。