Java 为什么在CPU共享缓存上更新非易失性变量?

Java 为什么在CPU共享缓存上更新非易失性变量?,java,multithreading,Java,Multithreading,flag变量不是易失性的,所以我希望在线程1处看到一个无限循环。但我不明白为什么Thread1可以在flag变量上看到Thread2的更新 为什么在CPU共享缓存上更新非易失性变量?这里的易失性和非易失性标志变量之间有区别吗 static boolean flag = true; public static void main(String[] args) { new Thread(() -> { while(flag){ System.

flag变量不是易失性的,所以我希望在线程1处看到一个无限循环。但我不明白为什么Thread1可以在flag变量上看到Thread2的更新


为什么在CPU共享缓存上更新非易失性变量?这里的易失性和非易失性标志变量之间有区别吗

static boolean flag = true;

public static void main(String[] args) {

    new Thread(() -> {
        while(flag){
            System.out.println("Running Thread1");
        }
    }).start();

    new Thread(() -> {
        flag = false;
        System.out.println("flag variable is set to False");
    }).start();

}

这样一个简单的程序将显示可感知的结果,这是零保证。我的意思是,它甚至不能保证哪个线程将首先启动,至少

但总的来说,可见性效果只有通过仔细构建所谓的“先发生后关系”才能得到保证。这是您唯一的保证,确切地说:

对易失性字段的写入发生在对该字段的每次后续读取之前

如果没有volatile,安全网就消失了。你可能会说,“但我无法复制”。答案是:

  • 。。。这一次

  • 。。。在这个平台上

  • 。。。使用此编译器

  • 。。。在这个CPU上

等等


事实上,您在其中添加了一个
System.out.println
(内部将有一个
synchronized
部分),这只会加剧问题;从某种意义上说,这会让一个线程永远运行下去,从而失去更多的机会


我花了一段时间,但我想我可以举一个例子来证明这一点。为此,您需要一个合适的工具:

您不需要理解代码(尽管这会有所帮助),但总体而言,它构建了两个“参与者”或两个线程,可以更改两个独立的值:
x
y
。有趣的是:

if(x == 2) {
     int local = y;
     result.r1 = x + local;
}
如果
x==2
,我们输入if分支和
结果。r1
应该总是
4
,对吗?如果
result.r1
3
,这是什么意思

这意味着
x==2
(否则根本不会写入
r1
,因此
result.r1
将为零),这意味着
y==1

这意味着
ThreadA
(或
writerThread
)执行了写入操作(我们确信
x==2
,因此
y
也应该是
2
),但
ThreadB
readerThread
)没有观察到
y
2
;它仍然将
y
视为
1

这些都是由
@结果(..)
定义的案例,显然我关心的是
3
。如果我运行这个(由您来决定如何运行),我将看到输出中确实存在
ACCEPTABLE\u interest
case

如果我只做了一次更改:

 private volatile int x = 1;
通过添加
volatile
,我开始遵循JLS规范。特别是该链接中的3点:

如果x和y是同一线程的动作,并且x在程序顺序中位于y之前,那么hb(x,y)

对易失性字段的写入发生在对该字段的每次后续读取之前

如果hb(x,y)和hb(y,z),那么hb(x,z)

这意味着,如果我看到
x==2
,我还必须看到
y==2
(不同于没有
volatile
)。如果我现在运行这个示例,
3
不会成为结果的一部分



这应该证明
非易失性
读取可以是快速的,因此会丢失,而
易失性
读取不能丢失。

在X86上,缓存总是一致的。因此,如果CPU1在地址a上执行存储,并且CPU2的缓存线包含,则在存储可以提交到CPU1上的L1D之前,CPU2上的缓存线将失效。因此,如果CPU2希望在缓存线失效后加载A,它将遇到一致性丢失,首先需要使缓存线处于共享或独占状态,然后才能读取A。因此,它将看到A的最新值

因此,不稳定的负载和存储不会影响缓存的一致性。在X86上,在CPU1将A提交给L1D之后,不会在CPU2上为A加载旧值

volatile的主要目的是防止针对其他加载和存储重新排序到其他地址。在X86上,几乎所有的重新排序都被禁止;由于存储缓冲,只有较旧的存储可以与较新的加载一起重新排序到不同的地址。防止这种情况的最明智的方法是在写入后添加[StoreLoad]屏障

有关更多信息,请参阅:

在JVM上,这通常使用“lock addl%(rsp),0”实现;这意味着0被添加到堆栈指针。但MFENCE同样有效。在硬件级别上发生的情况是,加载的执行停止,直到存储缓冲区耗尽为止;因此,较旧的存储需要全局可见(将其内容存储在L1D中),较新的加载才能全局可见(从L1D加载其内容),因此,阻止了较旧存储和较新加载之间的重新排序


附言:尤金上面说的是完全正确的。最好从Java内存模型开始,它是任何硬件的抽象(因此没有缓存)。除了CPU内存障碍,还有编译器障碍;因此,我上面的故事只提供了硬件上发生的高级别概述。我发现对硬件层面上发生的事情有一些线索是非常有见地的。

1。非易失性并不意味着更改不可见;没有花环。2.
println
涉及同步,这会影响可见性。
out.println()
在打印前缓冲文本。您可以使用
System.err.println()
立即获得结果“这里的易失性标志变量和非易失性标志变量之间是否存在差异?”可能。也许不是。如果它不是易失性的,那么JVM可以选择
 private volatile int x = 1;