volatile的作用

  • 保证变量在多线程中的可见性
  • 保证指令的有序性(禁止指令重排)

保证变量在多线程中的可见性

  1. 主存中有一个变量a, 它被volatile关键字所修饰, 并且它的值是0

  2. T1T2线程读取了变量a, 此时它们的线程的本地内存(CPU寄存器或高速缓存)中的值都是0

  3. 如果T1修改了变量a的值为1, 由于被a变量被volatile所修饰, 那么T1修改之后的a就会被刷新到主存(写屏障), 并且执行T2线程的CPU对a变量的缓存行会失效(读屏障), 强制从主存中获取值, 到达了多线程中变量的可见性的目的

  4. 通过内存屏障保证变量在多线程中的可见性, 如果字段被volatile修饰:

    • 读屏障: 在读指令前插入读屏障,可以让高速缓存中的数据失效重新从主内存加载数据
    • 写屏障: 在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存

保证指令的有序性(禁止指令重排)

指令重排的意义

  • JVM能够根据CPU的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥CPU的性能,提高效率。

  • 单线程情况下, 指令重排没问题, 但是多线程环境下, 指令重排可能造成程序的执行结果并不是我们所期待的, 比如单例模式的DCL写法对DCL单例模式的思考

volatile通过两点保证指令有序性

happens-before

happens-before原则(定义一些禁止编译优化的场景,保证并发编程的正确性), 下图来源于: 来源《Java并发编程实践》

内存屏障

  • 编译器生成字节码时, 会在指令序列中插入内存屏障,会多出一个 lock 前缀指令

  • 内存屏障是一组处理器指令,解决禁止指令重排序和内存可见性的问题,保证指令重排序后与之前的输出结果一 样,使性能得到优化, 处理器在进行重排序时是会考虑指令之间的数据依赖性

  • 先于这个内存屏障的指令必须先执行,后于这个内存屏障 的指令必须后执行

使用 volatile 的经典场景

  • 双重校验锁 DCL(double checked locking)
  • ConcurrentHashMap哈希数组Node[]
  • 原子类例如AtomicIntegervalue属性
  • 多个线程操作同一块内存的同一个变量(保证有序性和内存可见性,有序性和可见性同等重要,都不能忽略)