volatile底层原理

作者: 面向对象架构 | 来源:发表于2022-12-28 00:31 被阅读0次

    java并发编程中,经常会看到 volatile 关键字。今天就让我们来盘一盘它。

    volatile相关定义

    java语言规范对于 volatile 定义如下:
    java编程语言允许线程访问共享变量,为了确保共享变量能偶被准确和一致性地更新,线程应该确保通过排它锁单独获得这个变量。

    通俗讲,一个字段被 volatile 关键字修饰,java的内存模型却被所有的线程看到的这个变量值是一致的,但是它并不能保证多线程的原子操作。这就是所谓的线程可见性,但不保证原子性。

    Java共享变量的内存可见性问题

    Java内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作内存,然后对工作内存里的变量进行处理,处理完后将变量值更新到主内存。

    Java内存模型是一个抽象的概念,那么实际实现中工作内存类似于CPU的高速缓存,下面是一个双核CPU的系统架构。

    如图所示,每个内核都有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辑运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。

    Java内存模型里面的工作内存,就对应这里的L1或者L2缓存或者CPU的寄存器。

    在上图架构基础上,如果线程A和线程B同时处理一个共享变量,会出现什么情况?假设线程A和线程B使用不同CPU执行,并且当前量级Cache都为空,那么这时候由于Cache的存在,将会导致内存不可见问题,具体分析如下:

    • 线程A首先获取共享变量X的值,由于L1,L2Cache都没有命中,所以加载主内存中X的值,假如为0.然后把X=0的值缓存到L1,L2Cache,线程A修改X的值为1,然后将其写入量级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内合主内存里面的X的值都是1。
    • 线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X=1;到此处一切正常,因为这时候主内存中X=1。然后线程B修改X的值为2,并将其存到线程2所在的一级Cache和共享二级Cache中,最后更新主内存中X的值为2;到这里一切都是好的。
    • 线程A再次需要修改X的值,获取一级缓存命中,并且X=1,到这里问题就出现了,命名线程B已经把X的值修改为了2,为何线程A获取的还是1呢?这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。

    解决缓存一致性方案有两种:

    1. 通过在总线加LOCK#锁的方式;
    2. 通过缓存一致性协议。

    但是方案1存在一个问题,它是采用一种独占的方式来实现,即总线加LOCK#锁的话,只能有一个CPU能够运行,其他CPU都得阻塞,效率较为低下。

    第二种方案,缓存一致性协议(MESI)它确保每个缓存中使用的共享变量的副本是一致的,所有JMM就解决这个问题。

    volatile实现原理

    有volatile修饰的共享变量进行写操作时会多出Lock前缀的指令,该指令在多核处理器下会引发两件事情。

    1. 讲当前处理器缓存行数据刷写到系统驻内存。
    2. 这个刷写回主内存的操作会使其他CPU缓存的该共享变量内存地址的数据无效。

    这样就保证了多个处理器的缓存是一致的,对应的处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器缓存行设置无效状态,当处理器对这个数据进行修改操作时会重新从主内存中把数据读取到缓存里。

    volatile的第一个特性--保证可见性

    解决内存可见性问题方式的一种是加锁,但是使用锁太笨重,因为它会带来线程上下文的切换开销。Java提供了一种弱形式的同步,也就是volatile关键字。该关键字确保对一个变量的更新对其他线程马上可见。

    当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。

    当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。

    理解volatile保证可见性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。

    volatile的第二个特性--保证有序性

    Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。

    什么是数据依赖性?
    如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。

    在单线程下重排序可以保证最终执行结果与程序顺序执行的结果一致,但是在多线程下就会出现问题。

    使用场景

    volatile经常用于两个场景:状态标记、double check

    状态标记

    // 线程
    volatile boolean flag = false;
    while(!flag) {
    doSomething();
    }

    public void setFlag() {
    flag = true;
    }

    volatile boolean inited = false;

    // 线程1
    context = loadContext();
    inited = true;

    // 线程2
    while(!inited) {
    sleep();
    }
    doSomethingWithConfig(context);

    double check

    我们可以放心的使用DCL(double-checked locking)实现单例模式。

    public class Singleton2 {
    /**
    * 双重锁机制+volatile关键字实现单例
    */
    //声明单例对象
    private static volatile Singleton2 instance;

    //私有化构造器
    private Singleton2(){
    
    }
    //双重检查加锁
    public static Singleton2 getInstance(){
        if (instance == null) {
            synchronized (Singleton2.class) {
                if (instance == null) {
                    instance=new Singleton2();
                    }
            }
        }
        return instance;
    }
    

    }

    相关文章

      网友评论

        本文标题:volatile底层原理

        本文链接:https://www.haomeiwen.com/subject/asdixdtx.html