为什么在我的示例中Unsafe.fullFence()无法确保可见性?

我正在尝试深入Java中的volatile关键字并设置2个测试环境。我相信他们俩都使用x86_64并使用热点。

Java version: 1.8.0_232
CPU: AMD Ryzen 7 8Core

Java version: 1.8.0_231
CPU: Intel I7

代码在这里:

import java.lang.reflect.Field;
import sun.misc.Unsafe;

public class Test {

  private boolean flag = true; //left non-volatile intentionally
  private volatile int dummyVolatile = 1;

  public static void main(String[] args) throws Exception {
    Test t = new Test();
    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setaccessible(true);
    Unsafe unsafe = (Unsafe) f.get(null);

    Thread t1 = new Thread(() -> {
        while (t.flag) {
          //int b = t.someValue;
          //unsafe.loadFence();
          //unsafe.storeFence();
          //unsafe.fullFence();
        }
        System.out.println("Finished!");
      });

    Thread t2 = new Thread(() -> {
        t.flag = false;
        unsafe.fullFence();
      });

    t1.start();
    Thread.sleep(1000);
    t2.start();
    t1.join();
  }
}

“完成!”永远不会打印出来,这对我来说毫无意义。我期望线程2中的fullFence使flag = false全局可见。

根据我的研究,Hotspot使用lock/mfence在x86上实现fullFence。根据{{​​3}}

此序列化操作可确保以程序顺序在MFENCE指令之前的每个加载和存储指令在MFENCE指令之后的任何加载或存储指令之前都是全局可见的。

即使是“更糟糕”,如果我在线程2中注释掉fullFence并在线程1中取消注释xxxFence中的任何一个,代码也会打印出“完成!”。因为至少有Intel's instruction-set reference manual entry for mfence,所以这没有什么意义。

我的信息来源可能不准确,或者我误会了一些东西。请帮忙,谢谢!

xiejy1 回答:为什么在我的示例中Unsafe.fullFence()无法确保可见性?

无关紧要的运行时影响,而是强制编译器重新加载内容的编译时影响。

您的t1循环不包含volatile读取或任何可能与另一个线程进行同步的内容,因此无法保证它会永远注意到对任何变量。。例如,当将JIT插入asm时,编译器可以创建一个循环,将值一次加载到寄存器中,而不是每次都从内存中重新加载。您一直希望编译器能够对非共享数据进行这种优化,这就是为什么该语言具有在没有可能进行同步时允许它执行此操作的规则的原因。

然后当然可以将条件提升到循环之外。因此,没有障碍或任何障碍,您的阅读器循环可以将JIT插入实现此逻辑的asm

if(t.flag) {
   for(;;){}  // infinite loop
}

除了排序之外,Java volatile的另一部分还假定其他线程可以异步更改它,因此不能假定多次读取都给出相同的值。

但是unsafe.loadFence();使JVM每次迭代都从(缓存一致性)内存重新加载JVM t.flag。我不知道这是Java规范所必需的,还是仅仅只是使它起作用的实现细节。

如果这是带有非atomic变量的C ++(在C ++中这是未定义的行为),那么您会在GCC之类的编译器中看到完全相同的效果。 _mm_lfence也是编译时的全屏障,并且会发出无用的lfence指令,有效地告诉编译器所有内存可能已更改,因此需要重新加载。因此,它无法对负载进行重新排序,也无法将其吊起。

顺便说一句,我不确定unsafe.loadFence()甚至不支持x86上的lfence指令。它 对于内存排序没有用(除了非常模糊的东西,例如从WC内存中隔离NT负载,例如从视频RAM复制,而JVM可以认为这是没有发生的),因此针对x86的JVM JITing可以将其视为编译时的障碍。就像C ++编译器为std::atomic_thread_fence(std::memory_order_acquire);所做的一样-跨障碍阻止编译时对装入进行重新排序,但是不会发出asm指令,因为运行JVM的主机的asm内存已经足够强大。


在线程2中,unsafe.fullFence();毫无用处。它只是使那个线程等待,直到较早的存储在全局范围内可见,然后再进行任何较晚的加载/存储。 t.flag = false;是一个显而易见的副作用,无法进行优化,因此即使没有volatile,它也确实会在JITed组件中发生,无论是否存在障碍。而且它不能被延迟或与其他东西合并,因为同一线程中没有其他东西。

Asm存储区总是对其他线程可见,唯一的问题是当前线程在此线程中进行更多处理(尤其是加载)之前是否等待其存储缓冲区耗尽。即防止所有重新排序,包括StoreLoad。 Java volatile就像C ++ memory_order_seq_cst一样(通过在每个存储区之后使用完整的屏障)来做到这一点,但是没有屏障,它仍然像C ++ memory_order_relaxed一样存储。 (或者在JITing x86 asm上,加载/存储实际上与获取/发布一样强大。)

缓存是连贯的,并且存储缓冲区总是尽可能快地耗尽自身(提交到L1d缓存),以腾出空间来执行更多存储。


注意:我不了解Java,也不知道在一个线程中分配非volatile并在不同步的情况下在另一个线程中读取它是多么不安全/不确定。根据您所看到的行为,听起来完全像您在C ++中看到的一样,使用非atomic变量(启用优化,就像HotSpot总是这样做)一样。

(基于@Margaret的评论,我对我假设Java同步的工作方式进行了一些猜测,如果我误报了任何内容,请进行编辑或评论。)

在非atomic变量上的C ++数据竞争中,始终是未定义的行为,但是,当然,当编译真实的ISA(不进行硬件竞争的情况下)时,结果有时就是人们想要的。 >

本文链接:https://www.f2er.com/2787084.html

大家都在问