volatile
volatile 特点
- 保证可见性
- 不保证原子性
- 禁用指令重排
可见性
- JMM 内存模型
- 每一个 Java 程序都会有一个主内存空间
- 所有对象的分配都是在主内存空间中申请
- 每一个线程都会有自己的工作内存
- 当某个线程访问某个对象时, 会将该对象的数据拷贝一份到自己的工作内存
- 由于 JMM 内存模型的原因, 默认情况下各个线程在访问同一个变量时, 各个线程不可见
- 当变量被 volatile 修饰时, 某个线程在修改该变量后会通知其他线程修改这个变量, 这个叫做可见性
不保证原子性
- 当多个线程同时对一个变量进行修改操作时, volatile 修饰的变量所生成的字节码并不会同时执行完, 而是分成好几条指令, 他不能保证每次操作都是原子性的
- 解决方法: 使用 JUC 下的类
禁止指令重排
指令重排: 在单线程下, 编译器会分析代码的流程, 在不改变数据依赖的前提下, 会对指令进行重排, 这种重排在单线程下是没有问题的, 但是多线程下就可能出现问题
使用 volatile 修饰变量, 编译器会禁用指令重排, 严格按照代码的执行流程去执行
例:
- 单例模式:
- 一般单例模式使用的是双端检锁机制来保证安全的
- 双端检锁: 即获取实例时, 首先检测是否为空, 如果为空, 加锁, 然后再判断是否为空, 如果为空, 创建实例
- 一般而言创建一个对象都是由如下三个步骤完成:
- 申请内存空间
- 使用构造函数初始化内存
- 将内存地址返回给引用
- 但是由于指令重排的存在, 第二, 第三步骤可能被重排, 导致即使实例不为空, 但是该对象并没有初始化完成, 此时调用其实例方法, 依然会抛出空指针异常
- 解决方法: 使用 volatile 关键词修饰变量, 禁用编译器对指令重排
- 一般单例模式使用的是双端检锁机制来保证安全的
- 单例模式:
JUC 下的类
- 以 AtomicInteger 为例
- JUC 下的类给我们提供了内建数据类型能够保证原子性操作的方法
- 其实现原理为 CAS(此过程称为自旋锁)
- 首先取得待修改变量主内存的值
- 然后修改时比较取得的值与当前主内存的值是否相同
- 如果相同, 修改, 不同重复上面两个过程
- 自旋锁问题:(ABA 问题)
- 当有两个线程(A 和 B)同时修改一个变量
- 其中一个线程(A)运行速度慢, 而另一个线程(B)运行速度快
- 当 A 取得值时, B 将该值修改了两次, 并且第二次将值改回原来的内容
- 此时 A 在修改该值时, 会认为该值没有被改过, A 能修改成功
- 一般情况, 这个问题并不会有什么危害, 但是可能会出现想不到的问题
- 解决 ABA 问题:
- 在某个线程获取值时, 同时获取上次修改的时间戳(或版本号), 在修改时, 同时比较值和时间戳(版本号)即可避免 ABA 问题
- 自定义数据类型保证操作原子性的方法
- 使用原子引用(不能避免 ABA 问题): AtomicReference
- 使用带时间戳的原子引用(避免 ABA 问题): AtomicStampedReference
- 使用原子引用(不能避免 ABA 问题): AtomicReference