volatile的进一步思考

在深入理解volatile关键字的过程中,出现了一些之前没有了解到的知识点,而这些知识点有影响着我对volatile的认知,下面就对这些知识点做一些梳理

MESI是什么

为了解决速度不匹配问题,计算机中多处使用到了缓存。为了解决CPU和内存的速度不匹配问题,出现了高速缓存。在多核CPU的计算机中,每个CPU有自己单独的高速缓存。如果只是读或者在单核的情况下,缓存中的数据不会出现不一致的情况。只有当出现了写,才有可能出现缓存不一致问题。本质上不是CPU的核数不唯一造成的。而是多个缓存同时缓存了共享数据造成的。那么针对这个因为CPU造成的问题,CPU厂商自己做了一层在CPU缓存硬件级别的解决方案。这个就是MESI,虽然他们各个高速缓存之间是独立的,但是他们公用一条总线。每个缓存都在不停地嗅探总线中的任何一个操作。一个缓存通过总线去读取写入内存中的数据,其他缓存都会同步的使自己缓存中的数据失效或修改成其他状态。但是如果只是在自己的缓存中修改了,没有同步到内存,这个时候怎么办呢?其实可以提前一步,在CPU通知缓存修改的时候,也就是缓存修改之前就通过总线通知其他缓存,让他们修改状态。

详情请参考:https://www.infoq.cn/article/cache-coherency-primer

都已经有了MESI,volatile还有什么用呢?

MESI只是保证了高速缓存这一块的一致性,但是从内存到CPU之间还有其他的缓存。同样会出现可见性问题,而且不是所有的硬件体系架构都支持MESI。java的volatile关键字的作用可以认为是让所有缓存(例如寄存器)都失效(认为是不可靠的),针对volatile修饰的共享变量,读写都直接操作主内存。这样就从根本上杜绝了不一致问题。

volatile为什么要禁止重排序呢

  • 重排序一定要保证在单线程情况下,不影响最终的执行结果。

首先重排序在java编译期间会发生重排序,根据as-if-serial语义,会把指令重排序。重排序的目的是为了提升效率,他会尽可能的把对于同一变量的操作放到一起,减少缓存交互次数。

其次处理器也会做重排序,目前的处理器都才用了指令级并行技术,比如处理器的加法器和除法器器是可以同时执行的。所以同一时刻加法指令和除法指令是可以并行执行,但是两个加法指令需要顺序执行。为了有效的利用CPU,硬件级别也会做重排序。

重排序在单线程的情况下不会出现问题,但是在多线程的情况下会有问题。比如说双重检查锁来实现的单例模式。

public class Singleton {

    private volatile static Singleton singleton;

    private Singleton(){}

    public static Singleton getInstance() {
        // 这里主要原因是synchronized是一个重量操作,这样可以减少锁操作
        if (singleton == null) {
            // 如果不加锁的话,就会出现线程1判断了为null以后,时间片切换了,然后线程2来判断也为null的情况
            synchronized (Singleton.class) {
                // 如果不判断的话,当线程1和线程2同时过来,线程1获取了锁资源,线程2挂起到等待队列,等线程1执行完释放锁以后,线程2也会创建
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
}

这里加上volatile的作用是singleton = new Singleton();不是一个原子操作。他可能出现重排序。比如先申请了内存,然后把内存地址就返回给变量。但这个时候是没有实例化的,等另外一个线程过来发现singleton不为null,然后就返回使用了。但这个时候对象还没有实例化完成,就会出现问题。这就是重排序造成的问题。volatile的作用就是禁止重排序。

参考

https://blog.csdn.net/aigoogle/article/details/40793947

https://www.jianshu.com/p/35e4504d42e4

猜你喜欢

转载自www.cnblogs.com/colin-xun/p/11550028.html