0%

volatile

volatile

volatile 特点

  • 保证可见性
  • 不保证原子性
  • 禁用指令重排

可见性

  1. JMM 内存模型
    • 每一个 Java 程序都会有一个主内存空间
    • 所有对象的分配都是在主内存空间中申请
    • 每一个线程都会有自己的工作内存
    • 当某个线程访问某个对象时, 会将该对象的数据拷贝一份到自己的工作内存
  2. 由于 JMM 内存模型的原因, 默认情况下各个线程在访问同一个变量时, 各个线程不可见
  3. 当变量被 volatile 修饰时, 某个线程在修改该变量后会通知其他线程修改这个变量, 这个叫做可见性

不保证原子性

  • 当多个线程同时对一个变量进行修改操作时, volatile 修饰的变量所生成的字节码并不会同时执行完, 而是分成好几条指令, 他不能保证每次操作都是原子性的
  • 解决方法: 使用 JUC 下的类

禁止指令重排

  • 指令重排: 在单线程下, 编译器会分析代码的流程, 在不改变数据依赖的前提下, 会对指令进行重排, 这种重排在单线程下是没有问题的, 但是多线程下就可能出现问题

  • 使用 volatile 修饰变量, 编译器会禁用指令重排, 严格按照代码的执行流程去执行

  • 例:

    1. 单例模式:
      • 一般单例模式使用的是双端检锁机制来保证安全的
        • 双端检锁: 即获取实例时, 首先检测是否为空, 如果为空, 加锁, 然后再判断是否为空, 如果为空, 创建实例
      • 一般而言创建一个对象都是由如下三个步骤完成:
        1. 申请内存空间
        2. 使用构造函数初始化内存
        3. 将内存地址返回给引用
      • 但是由于指令重排的存在, 第二, 第三步骤可能被重排, 导致即使实例不为空, 但是该对象并没有初始化完成, 此时调用其实例方法, 依然会抛出空指针异常
      • 解决方法: 使用 volatile 关键词修饰变量, 禁用编译器对指令重排

JUC 下的类

  • 以 AtomicInteger 为例
  1. JUC 下的类给我们提供了内建数据类型能够保证原子性操作的方法
  2. 其实现原理为 CAS(此过程称为自旋锁)
    • 首先取得待修改变量主内存的值
    • 然后修改时比较取得的值与当前主内存的值是否相同
    • 如果相同, 修改, 不同重复上面两个过程
  3. 自旋锁问题:(ABA 问题)
    • 当有两个线程(A 和 B)同时修改一个变量
    • 其中一个线程(A)运行速度慢, 而另一个线程(B)运行速度快
    • 当 A 取得值时, B 将该值修改了两次, 并且第二次将值改回原来的内容
    • 此时 A 在修改该值时, 会认为该值没有被改过, A 能修改成功
    • 一般情况, 这个问题并不会有什么危害, 但是可能会出现想不到的问题
  4. 解决 ABA 问题:
    • 在某个线程获取值时, 同时获取上次修改的时间戳(或版本号), 在修改时, 同时比较值和时间戳(版本号)即可避免 ABA 问题
  5. 自定义数据类型保证操作原子性的方法
    • 使用原子引用(不能避免 ABA 问题): AtomicReference
    • 使用带时间戳的原子引用(避免 ABA 问题): AtomicStampedReference