volatile关键字
简介
volatile 关键字可以说是 Java 虚拟机提供的最轻量级的同步机制了
volatile 通过实现 内存可见性 和 禁止重排序 来解决并发问题,但是volatile关键字**不保证并发原子性**
内存可见性
可见性是指:当一个线程修改了共享变量的值,其它线程能立即感知到这种变化
JMM 规定:
-
volatile
变量的每次修改都必须立即回写到主内存中-
写一个 volatile 属性会立即刷入到主内存
-
在对 volatile 修饰的变量进行写操作的时候,会在写之后将工作空间的变量值更新到主内存中
-
-
volatile
变量的每次使用都必须从主内存刷新最新的值- 读一个 volatile 变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值
- 在对 volatile 修饰的变量进行读操作的时候,会先加载(load)主内存中最新的值
保证内存可见性的动作
volatile 关键字保证每个线程操作数据的时候会把数据从主内存读取到自己的工作内存,如果其中一个线程操作了数据并且更新了,那么其他已经读取的线程的变量副本就会失效了,需要都数据进行操作又要再次去主内存中读取了
为了解决这种一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作;这类协议有 MESI ,MSI 等
MESI(缓存一致性协议)
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态
因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取
每个处理器通过 嗅探 在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里
由于 volatile 的 MESI 缓存一致性协议,需要不断的从主内存嗅探和 cas 不断循环,无效交互会导致总线带宽达到峰值;从而带来性能问题
禁止重排序
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序
一般重排序可以分为如下三种:
编译器优化的重排序
- 编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
指令级并行的重排序
- 现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
内存系统的重排序
- 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的
volatile 变量是禁止重排序的,它能保证程序实际运行是按代码顺序执行的
volatile 通过插入 内存屏障 指令来禁止特定类型的处理器重排序
volatile 变量的影响范围不仅仅只包含它自己,它会对其上下的变量值的读写都有影响
内存屏障
内存屏障有两个作用:
-
阻止屏障两侧的指令重排序
-
强制把 写缓冲区/高速缓存 中的数据回写到主内存,让缓存中相应的数据失效
volatile 写操作是在前面和后面分别插入内存屏障
volatile 读操作是在后面插入两个内存屏障
双重检查单例
public class Singleton {
//volatile修饰
private volatile static Singleton instance = null;
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
//双重检查
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private Singleton() {}
}
对象实际上创建对象要进过如下几个步骤:
- 分配内存空间
- 调用构造器,初始化实例
- 返回地址给引用
如果没有禁止重排序,那么会会发生分配完内存空间后,直接返回了内存地址的引用,此时对象还没有初始化完毕,当其他线程判断 instance != null
时,直接就返回了半成品的对象,这就会出现问题了
小结
-
适用场景
-
某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值
-
使用场景必须本身就是原子的
-
-
volatile 属性的读写操作都是无锁的,它不能替代 synchronized,因为它 不提供原子性和互斥性
- 因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的
-
volatile 只能作用于属性
- 用 volatile 修饰属性,这样编译器就不会对这个属性做指令重排序
-
volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见
- volatile 属性不会被线程缓存,始终从主存中读取
-
volatile 可以使得 long 和 double 的赋值是原子的
-
long 和 double 类型,它们的值需要占用 64 位的内存空间
-
Java 编程语言规范中提到,对于 64 位的值的写入,可以分为两个 32 位的操作进行写入
-
本来一个整体的赋值操作,被拆分为低 32 位赋值和高 32 位赋值两个操作,中间如果发生了其他线程对于这个值的读操作,必然就会读到一个奇怪的值
-
synchronized 和 volatile 的区别
- volatile 可以修饰实例变量和类变量,而 synchronized 修饰方法和代码块
- volatile 保证数据的可见性,但不能保证数据的原子性
- synchronized 是互斥机制
- 多线程访问 volatile 关键字不会发生阻塞,而 synchronized 关键字可能会发生阻塞