• volatile 关键字


    如何保证变量的可见性

    • 在 Java 中,volatile 关键字可以保证变量的可见性,如果变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取

    如何禁止指令重排

    • 在 Java 中,volatile 还有一个重要的作用就是防止 JVM 的指令重排序,如果将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的内存屏障的方式来禁止指令重排序

    • 在 Java 中,Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:
      public native void loadFence();
      public native void storeFence();
      public native void fullFence();

      理论上来说,通过这个三个方法也可以实现和 volatile 禁止重排序一样的效果,只是会麻烦一些

    • 禁止指令重排效果演示

    // 双重检查实现单例模式
    public class Singleton {
    
        private volatile static Singleton uniqueInstance;
    
        private Singleton() {
        }
    
        public  static Singleton getUniqueInstance() {
           //先判断对象是否已经实例过,没有实例化过才进入加锁代码
            if (uniqueInstance == null) {
                //类对象加锁
                synchronized (Singleton.class) {
                    if (uniqueInstance == null) {
                        uniqueInstance = new Singleton();
                    }
                }
            }
            return uniqueInstance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 执行步骤 (uniqueInstance 采用 volatile 修饰)
    uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
    为 uniqueInstance 分配内存空间
    初始化 uniqueInstance
    将 uniqueInstance 指向分配的内存地址
    • 由于 JVM 具有指令重排的特性,执行顺序有可能变成 1 ⇒ \Rightarrow 3 ⇒ \Rightarrow 2

    • 指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例

    • 例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化


    volatile 是否可以保证原子性

    • volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的

    • 假设对变量 inc 进行 ++ 操作,inc++ 其实是一个复合操作,包括三步:
      读取 inc 的值
      对 inc 加 1
      将 inc 的值写回内存

    • volatile 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:
      线程 1 对 inc 进行读取操作之后,还未对其进行修改
      线程 2 又读取 inc 的值并对其进行修改(+1),再将 inc 的值写回内存
      线程 2 操作完毕后,线程 1 对 inc 的值进行修改(+1),再将 inc 的值写回内存

    • 这就导致两个线程分别对 inc 进行一次自增操作后,inc 实际上只增加 1

    • 如果想要保证上面的代码运行正确也非常简单,利用 synchronized 、Lock 或者 AtomicInteger 都可以


    内存屏障

    • 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为以下三种:源代码 ⇒ \Rightarrow 编译器优化的重排 ⇒ \Rightarrow 指令并行的重排 ⇒ \Rightarrow 内存系统的重排 ⇒ \Rightarrow 最终执行指令

    • 单线程环境最终执行结果和代码顺序的结果一致

    • 处理器在进行重排序时,必须要考虑指令之间的数据依赖性

    • 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

    • volatile 实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象

    • 内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:保证特定操作的顺序 和 保证某些变量的内存可见性
      1、由于编译器和处理器都能执行指令重排的优化,如果在程序中插入一条Memery Barrier(内存屏障),那么就会告诉编译器和CPU不能对这条指令进行重排
      2、也就是说通过插入内存屏障,使屏障前后的指令不会进行重排优化
      3、内存屏障还可以强制刷出CPU的缓存,因此CPU上的线程都能读到这些数据的最新版本

    • 内存屏障分为四种
      StoreStore 屏障、StoreLoad 屏障、LoadLoad 屏障、LoadStore 屏障

    • Load 相当于读屏障,Store 相当于写屏障

    • 工作流程


    参考文章

  • 相关阅读:
    UE4 Android、IOS监听应用状态
    11.摆花
    结合Elementplus源码讲解BEM的使用
    如何找回回收站清空的文件?
    【LeetCode 130. 被围绕的区域】
    网络安全笔记 -- XSS跨站(原理、分类)
    如何使用测试仪进行400G交换机性能测试
    2022TWS蓝牙耳机推荐,盘点600元真无线蓝牙耳机
    适用于多种场景功能强大的在线海报图片素材设计器源码
    Opentelemetry SDK的简单用法
  • 原文地址:https://blog.csdn.net/qq_41956014/article/details/127820621