Volatile真正的原理

174 阅读4分钟

最近在重温并发编程的内容时,遇到synchronized和volatile的实现原理。其中关于volatile原理部分看了很多文章和视频。并没有真正解决我心中的疑惑。特此写一些自己的思考。希望大家可以指正。

问题:volatile 是如何实现可见性和有序性?

1:关于可见性的回答,我看到很大一部分人总会说到Java内存模型(JMM)和缓存一致性协议。 一个比较常见的例子

static volatile boolean stop = false;

public static void main(String[] args) {

    new Thread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        stop = true;
    }).start();

    foo();
}

static void foo() {
    while (!stop) {
    }
    System.out.println("停止");
}

不少观点指出: stop 变量一旦用volatile关键字修饰以后 foo方法就可以停止,而不用volatile修饰就永远不会停止的原因是:volatile修饰变量以后,变量的内容就会回写到主内存,从而触发缓存一致性协议,使得主线程能读到最新的值。

这种观点非常具有迷惑性,但是根本站不住脚。首先从逻辑上来说,缓存一致性协议是解决:cpu和内存交互时为了解决压榨cpu速度,引用缓存后进而引发的缓存数据不一致问题。也就是说有没有volatile,缓存一致性协议它都一直在那里发挥自己的作用。其次用实验证明:我只要在上述代码中再启动一个线程读取stop的值就知道到底有没有回写到主内存中

image.png 显然新开的线程是可以读取到最新值的。但是为什么主线程读取不到呢?

我们再做一个实验,我在foo方法的while循环内部阻塞

static void foo() {
    while (!stop) {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    System.out.println("停止");
}

这是我们发现就算不加volatile,程序也能停下来。 其实本质原因是在于Java即时编译器(JIT)将while这部分代码做了优化。while(!stop) 为true的次数过多时,JIT直接将while(!stop)默认为true。不会再去从内存中读取新的值。可以通过增加JVM参数 -Xint 关闭JIT优化。这样不加volatile也能使程序停止下来。

说到这里可能有点懵了,不是说volatile可以保证可见性吗?怎么感觉没有volatile没有关系呢?所以说volatile实现了可见性。这个结论本身肯定没有问题。但是说的宽泛了。我个人觉得应该是:volatile实现了JMM层面的可见性。而缓存一致性协议是真正实现了cpu和内存层面的可见性。这样理解起来就容易多了。

那么volatile对于可见性的作用到底是如何体现的呢? 首先,被volatile修饰的关键字在在汇编层面执行时会被加上lock前缀。JIT对于这种lock前缀的指令,是不会强制优化的。还是会每次从内存读取数据。 其次,加有lock前缀的指令执行后。会立马将缓存行回写到主内存。换句话说,加了volatile关键字会立马体现出cpu和内存层面的可见性。但是体现可见性的本质还是由缓存一致性协议做到的。volatile和缓存一致性协议的关系,说具体点姑且能大致理解成lock和MESI的关系。实际上这两者是根本没有关系。

2:关于有序性的回答 一个比较常见的例子 双检锁还需要加volatile 不然可能取到没有初始化完成的对象

private static volatile Singleton4 INSTANCE = null;

public static Singleton4 getInstance() {
    if (INSTANCE == null) {
        synchronized (Singleton4.class) {
            if (INSTANCE == null) {
                INSTANCE = new Singleton4();
            }
        }
    }
    return INSTANCE;
}

相当一部分的观点解释为:在volatile操作前后加上内存屏障来禁止指令重排序。并且列举JVM里关于内存屏障的规范和可能发生重排序的可能性。 那么在这里说明下什么是指令重排序。简单来说就是为了提高指令执行效率,执行的顺序会改变。分别为编译器优化重排序指令级并行重排序内存系统重排序 编译器好理解就是java层面,指令和内存系统的重排序都是在处理器级别。指令级是cpu执行的指令时会改变顺序,内存系统则是cpu在处理缓存数据的一致时的乱序。 在JVMl规范中确实有内存屏障的要求,但是追究到hotspot实现,其本质还是在操作volitale修饰的变量之前用到了lock前缀

lock前缀强制所有lock信号之前的指令,都在此之前被执行,并同步相关缓存。
强制所有lock信号之后的指令,都在此之后被执行,并同步相关缓存。

所以volatile实现有序性的本质并不是使用了内存屏障而是利用lock前缀,起到了内存屏障的效果

总结

与其说volatile的原理和缓存一致性协议,内存屏障相关。不如先学习lock前缀的作用。再回过头来研究MESI,store buffer,invalidate queue 可能会更好理解!

欢迎指正