问题根源
在多线程运行场景中,每个线程可能运行与 不同的CPU中,每个线程的读写都是操作自己CPU所在的高速缓存,但操作的数据存储在堆中,作为共享变量,每个线程操作的实际上是自己本地线程的副本,在并发 编程中,可能就会存在缓存不一致的问题
早期的CPU是通过总线加Lock#锁的方式解决缓存不一致问题,对总线加Lock#锁只保证只能有一个CPU使用这个变量的内存,这样会导致其他CPU无法访问内存,效率低,现在主要是通过缓存一致性协议来解决缓存一致性问题
Volatile特征
- Volatile保证了不同线程对该线程操作的内存可见性
- 禁止编译器进行指令性重排
- volatile变量是一种轻量同步机制,访问volatile不会执行加锁操作
- volatile无法同事保证内存可见性和 原子性,因此更建议使用加锁(同步)机制
Volatile解决内存可见性问题
之前我们分析过 ,多线程的读写并不会操作主内存(共享内存),而是操作线程的本地副本,这也是导致线程间数据不可见的本质,Volatile主要是从这个角度解决问题
- 修改volatile修饰的变量,CPU会强制将修改后的值刷新到主内存中
- 修改volatile修饰的变量,会导致其他线程 的本地内存的对应该变量值失效,该变量值的获取需要再重新从共享内存中读取
如何防止编译器指令性重排
编译器语义重排的后果
编译器遵守as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
Public void reader() {
if (flag) { //3
int i = a * a; //4
……
}
}
}
如图所示,操作1和操作2做了重排序。程序执行时,线程A首先写标记变量 flag,随后线程 B 读这个变量。由于条件判断为真,线程 B 将读取变量a。此时,变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了!
volatile关键字保证有序性
* volatile关键字关键字实际上给CPU添加了一个内存屏障,它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
volatile使用要点
-
对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
-
该变量没有包含在具有其他变量的不变式中。