• 【Java并发编程 】同步——volatile 关键字


    /ˈvɒlətaɪl/  我了太噢(记不住单词怎么读)
    
    • 1

    一、volatile的介绍?

    volatile是一个轻量级的synchronized,一般作用与变量,在多处理器开发的过程中保证了内存的可见性。相比于synchronized关键字,volatile关键字的执行成本更低,效率更高。

    • 对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
    • 从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。

    volatile 常用于多线程环境下的单次操作(单次读或者单次写)。

    二、volatile的特性有哪些?

    并发编程的三大特性为可见性、有序性和原子性。通常来讲volatile可以保证可见性和有序性。

    • 原子性:对于单个的volatile修饰的变量的读写是可以保证原子性的,但对于i++这种复合操作并不能保证原子性。这句话的意思基本上就是说volatile不具备原子性了。
    • 可见性:volatile可以保证不同线程对共享变量进行操作时的可见性。即当一个线程修改了共享变量时,另一个线程可以读取到共享变量被修改后的值。
    • 有序性:volatile会通过禁止指令重排序进而保证有序性。

    三、Java 中能创建 volatile 数组吗?

    能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。

    四、voliatile的实现原理?

    1. volatile实现内存可见性原理

    volatile的实现原理也是围绕如何实现可见性和有序性展开的。

    导致内存不可见的主要原因就是Java内存模型中的本地内存和主内存之间的值不一致所导致,例如上面所说线程A访问自己本地内存A的X值时,但此时主内存的X值已经被线程B所修改,所以线程A所访问到的值是一个脏数据。那如何解决这种问题呢?

    volatile可以保证内存可见性的关键是volatile的读/写实现了缓存一致性,缓存一致性的主要内容为:

    • 由于缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。
    • 当处理器写数据时,如果发现操作的是共享变量,会通知其他处理器将该变量的缓存设为无效状态。

    MESI(缓存一致性协议):当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,就会从内存重新读取。

    那缓存一致性是如何实现的呢?可以发现通过volatile修饰的变量,生成汇编指令时会比普通的变量多出一个Lock指令,这个Lock指令就是volatile关键字可以保证内存可见性的关键,它主要有两个作用:

    • 将当前处理器缓存的数据刷新到主内存。
    • 刷新到主内存时会使得其他处理器缓存的该内存地址的数据无效。

    2. volatile实现有序性原理

    为了实现volatile的内存语义,编译器在生成字节码时会通过插入内存屏障来禁止指令重排序。

    内存屏障:内存屏障是一种CPU指令,它的作用是对该指令前和指令后的一些操作产生一定的约束,保证一些操作按顺序执行

    Java虚拟机插入内存屏障的策略
    Java内存模型把内存屏障分为4类,如下表所示:

    屏障类型指令示例说明
    LoadLoad BarriersLoad1;LoadLoad;Load2保证Load1数据的读取先于Load2及后续所有读取指令的执行
    StoreStore BarriersStore1;StoreStore;Store2保证Store1数据刷新到主内存先于Store2及后续所有存储指令
    LoadStore BarriersLoad1;LoadStore;Store2保证Load1数据的读取先于Store2及后续的所有存储指令刷新到主内存
    StoreLoad BarriersStore1;StoreLoad;Load2保证Store1数据刷新到主内存先于Load2及后续所有读取指令的执行

    注:StoreLoad Barriers同时具备其他三个屏障的作用,它会使得该屏障之前的所有内存访问指令完成之后,才会执行该屏障之后的内存访问命令。

    Java内存模型对编译器指定的volatile重排序规则为:

    • 当第一个操作是volatile读时,无论第二个操作是什么都不能进行重排序。
    • 当第二个操作是volatile写时,无论第一个操作是什么都不能进行重排序。
    • 当第一个操作是volatile写,第二个操作为volatile读时,不能进行重排序。

    根据volatile重排序规则,Java内存模型采取的是保守的屏障插入策略,volatile写是在前面和后面分别插入内存屏障,volatile读是在后面插入两个内存屏障,具体如下:

    • volatile读:在每个volatile读后面分别插入LoadLoad屏障及LoadStore屏障(根据volatile重排序规则第一条),如下图所示
      在这里插入图片描述
      LoadLoad屏障的作用:禁止上面的所有普通读操作和上面的volatile读操作进行重排序。
      LoadStore屏障的作用:禁止下面的普通写和上面的volatile读进行重排序。
    • volatile写:在每个volatile写前面插入一个StoreStore屏障(为满足volatile重排序规则第二条),在每个volatile写后面插入一个StoreLoad屏障(为满足volatile重排序规则第三条),如下图所示

    在这里插入图片描述
    StoreStore屏障的作用:禁止上面的普通写和下面的volatile写重排序
    StoreLoad屏障的作用:防止上面的volatile写与下面可能出现的volatile读/写重排序。

    五、volatile能使一个非原子操作变成一个原子操作吗?

    volatile只能保证可见性和有序性,但可以保证64位的long型和double型变量的原子性。

    对于32位的虚拟机来说,每次原子读写都是32位的,会将long和double型变量拆分成两个32位的操作来执行,这样long和double型变量的读写就不能保证原子性了。

    因此,通过volatile修饰的long和double型变量则可以保证其原子性

    所以从Oracle Java Spec里面可以看到:

    • 对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。
    • 如果使用volatile修饰long和double,那么其读写都是原子操作
    • 对于64位的引用地址的读写,都是原子操作
    • 在实现JVM时,可以自由选择是否把读写long和double作为原子操作
    • 推荐JVM实现为原子操作

    六、volatile 修饰符的有过什么实践?

    单例模式

    是否 Lazy 初始化:是

    是否多线程安全:是

    实现难度:较复杂

    描述:对于Double-Check这种可能出现的问题(当然这种概率已经非常小了,但毕竟还是有的嘛~),解决方案是:只需要给instance的声明加上volatile关键字即可volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障(什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。注意:volatile阻止的不是singleton = newSingleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。

    public class Singleton7 {
    
        private static volatile Singleton7 instance = null;
    
        private Singleton7() {}
    
        public static Singleton7 getInstance() {
            if (instance == null) {
                synchronized (Singleton7.class) {
                    if (instance == null) {
                        instance = new Singleton7();
                    }
                }
            }
    
            return instance;
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    七、volatile 变量和 atomic 变量的区别?

    volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。

    而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

    八、volatile和synchronized的区别?

    volatile 表示:变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。

    synchronized 表示:只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。

    • volatile主要是保证内存的可见性,即变量在寄存器中的内存是不确定的,需要从主存中读取。synchronized主要是解决多个线程访问资源的同步性。
    • volatile作用于变量,synchronized作用于代码块或者方法。
    • volatile仅可以保证数据的可见性,不能保证数据的原子性。synchronized可以保证数据的可见性和原子性。
    • volatile不会造成线程的阻塞,synchronized会造成线程的阻塞。
    • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量。而synchronized关键字可以修饰方法以及代码块。
    • synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。

    九、volatile的典型应用场景与实战案例

    volatile 除了用于保障 long/ double 型变量的读、写操作的原子性,其典型使用场景还包括以下几个方面:

    • 使用volatile 变量作为状态标志
    • 使用volatile 保障可见性
    • 使用volatile 变量替代锁
    • 使用volatile 实现简易版读写锁
  • 相关阅读:
    探索小程序的世界(专栏导读、基础理论)
    ABAP 一般采购申请创建、服务类型采购申请创建BAPI_REQUISITION_CREATE
    Session会话追踪的实现机制
    STL的pair知识点大全
    IT部门不想每天忙“取数”,花了几百万买系统,还是这个办法有效
    计算机毕业设计Java酒店互联网平台系统(源码+mysql数据库+系统+lw文档)
    LeetCode 43. 字符串相乘
    记录一次压测性能调优
    精品基于Uniapp+SSM实现的实验室设备预约管理APP
    二进制类RPC协议
  • 原文地址:https://blog.csdn.net/weixin_44735928/article/details/126373495