首页 > 解决方案 > 为什么非易失性变量在 CPU 共享缓存上更新?

问题描述

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

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

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();

}

标签: javamultithreading

解决方案


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

但总的来说,可见性效果仅由java 语言规范来保证,该规范精心构建了所谓的“先发生关系”。这是您拥有的唯一保证,并且确切地说:

对 volatile 字段的写入发生在对该字段的每次后续读取之前。

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

  • ...在这次运行中

  • ...在这个平台上

  • ...使用此编译器

  • ... 在这个 CPU 上

等等。


您在其中添加 a System.out.println(内部将有synchronized一部分)的事实只会使事情恶化;从某种意义上说,它消除了让一个线程永远运行的更多机会。


我花了一段时间,但我我可以想出一个例子来证明这可以打破。为此,您需要一个合适的工具:专为这类事情而设计

@JCStressTest
@State
@Outcome(id = "0", expect = Expect.ACCEPTABLE)
@Outcome(id = "3", expect = Expect.ACCEPTABLE_INTERESTING, desc = "racy read!!!")
@Outcome(id = "4", expect = Expect.ACCEPTABLE, desc = "reader thread sees everything that writer did")
public class NoVolatile {

    private int y = 1;
    private int x = 1;

    @Actor
    public void writerThread() {
        y = 2;
        x = 2;
    }

    @Actor
    public void readerThread(I_Result result) {
        if(x == 2) {
            int local = y;
            result.r1 = x + local;
        }
    }
}

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

if(x == 2) {
     int local = y;
     result.r1 = x + local;
}

如果x == 2,我们输入 if 分支并且result.r1应该总是4,对吗?如果result.r13,这是什么意思?

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

这意味着ThreadA(or writerThread) 已经执行了一个操作(我们肯定知道x == 2and 因此y也应该是2),但是ThreadB( readerThread) 没有观察到y2; 它仍然被y视为存在1

这些是由那个定义的案例@Outcome(....),显然我关心的是那个3。如果我运行它(由你决定如何运行),我会看到ACCEPTABLE_INTERESTING输出中确实存在这种情况。

如果我进行一次更改:

 private volatile int x = 1;

通过添加volatile,我开始遵循 JLS 规范。特别是该链接的 3 点:

如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)。

对 volatile 字段的写入发生在对该字段的每次后续读取之前。

如果 hb(x, y) 和 hb(y, z),则 hb(x, z)。

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


这应该证明non-volatile读取可能是活泼的,因此会丢失,而volatile一个 - 不能丢失。


推荐阅读