Volatile的作用
现在的CPU都是多核,程序可以在多核上并行执行,指令在执行时,数据会从主存拷贝好CPU的各级缓存上,执行完之后在写回到主存上,此时同一个变量可能在两个核上被操作,该变量在两个拷贝分布在两个核上,此时就会出现问题,比如简单的自增操作,就会是你做你的,我做我的,最后结果会偏离预期。
使用volatile修饰共享变量后,每个线程要操作变量时,会把变量拷贝到缓存中,当线程操作变量副本写回主存后,会通过CPU总线嗅探机制告知其他线程该变量副本已经失效,需要从主存中重新获取。
Volatile是怎么实现的可见性
总线嗅探机制:CPU和内存有极大速度差距,如果CPU和内存直接通信,在存取的过程中CPU会一直空闲。所以CPU和内存之间设置的高速缓存,多缓存导致数据的不一致。总线嗅探就是每个CPU通过监听总线上传播的数据,来检查自己的缓存值是否过期,如果处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存状态置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。
对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag 这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的。
什么是指令重排
为了提高性能,在保证单线程下执行结果不变的情况,编译器和处理器通常会对指令进行重排序。
一般重排序分为三种类型:
- 编译器优化重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
Volatile防止指令重排
内存屏障:内存屏障是一组处理器指令,它的作用是禁止指令重排序和解决内存可见性问题。
JMM把内存屏障分为以下四类
volatile读和写是如何插入内存屏障规则:
- 在每个 volatile 读操作的后面插入 LoadLoad 屏障和 LoadStore 屏障。
- 在每个 volatile 写操作的前后分别插入一个 StoreStore 屏障和一个 StoreLoad 屏障。
也就是说,编译器不会对 volatile 读与 volatile 读后面的任意内存操作重排序;编译器不会对 volatile 写与 volatile 写前面的任意内存操作重排序。
总结
- volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值;或者作为状态变量,如 flag = ture,实现轻量级同步。
- volatile 属性的读写操作都是无锁的,它不能替代 synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
- volatile 只能作用于属性,我们用 volatile 修饰属性,这样编译器就不会对这个属性做指令重排序。
- volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。
- volatile 提供了 happens-before 保证,对 volatile 变量 V 的写入 happens-before 所有其他线程后续对 V 的读操作。
- volatile 可以使纯赋值操作是原子的,如
boolean flag = true; falg = false
。 - volatile 可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。