volatile 解析
volatile
是Java 虚拟机提供的最轻量级的同步机制,它有两个功能:
- 保证此变量对所有线程的可见性,可见性的意思是当一个线程修改这个变量的值,新值对于其他线程来说是可以立即得知的。即 Java 内存模型确保所有线程看到这个变量的值是一致的。
- 禁止指令重排序优化。普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。这就是Java内存模型中描述的”线程内表现为串行的语义“。
为什么会不一致?
- 从硬件角度看:在
多处理器
中每个处理器有各自的L1,L2等高速缓存。为了提高运行速度,刚用过的变量值会暂时存储在L1/L2高速缓存而不是立刻写回内存中(多处理器用相同的内存),这样如果一个处理器修改了某个变量而只写入自己的L1/L2高速缓存中,其他处理器就看不到这个修改值,这样就会造成不一致。 - 从JMM的角度看:JMM 规定了每个线程有自己的工作内存,如果工作内存不及时写回主内存进行同步,则各个线程相同的变量可能存在不同值。
什么是指令重排序?
指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。但并不是说指令任意重排,CPU需要能正确处理指令依赖情况以保障程序能得出正确的执行结果。比如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3把地址B中的值减去3,这时指令1和指令2是有依赖的,它们之间的顺序不能重排,但指令3可以重排到指令1、 2之前或者中间,只要保证CPU执行后面依赖到A、 B值的操作时能获取到正确的A和B值即可。
解决方法
就是把变量声明为volatile
类型,如果对声明了volatile
的变量进行写操作,会向处理器发送一条Lock前缀的指令,作用:
- 将这个变量所在的缓存行的数据写回主内存。
- Lock 操作相当于一个内存屏障,指令重排序时不能把Lock后面的指令重排序到内存屏障之前的位置。
Lock前缀的指令在多核处理器下会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。(缓存一致性协议)
使用 volatile
的优化:一般L1/L2/L3的高速缓存行是64个字节宽,不支持部分填充缓存行,即64字节的数据同时锁定,同时写回内存,同时在其他CPU的高速缓存中失效。将两个经常分别读写的变量分到不同缓存行中,不至于同时被失效,影响效率。
由于volatile
变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证原子性。
- 运算结果并不依赖变量的当前值(比如
setValue()
方法不依赖当前值,++
操作符则依赖当前值),或者能够确保只有单一的线程修改变量的值。 - 变量不需要与其他的状态变量共同参与不变约束。
volatile 用法
Java 里面的运算并非原子操作,volatile变量的运算不能保证原子性。
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race++;
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
});
threads[i].start();
}
//等待所有累加线程都结束
while (Thread.activeCount() > 1)
Thread.yield();
System.out.println(race);
}
}
这里的 race++;
并不是原子操作,volatile只保证了变量的可见性,所以这里是错误的用法。
volatile 的使用场景
volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
//do stuff
}
}
这里 shutdown()
方法被调用时,能保证所有线程中执行的 doWork()
方法都立即停下来。
volatile 的使用场景二
Map configOptions;
char[]configText;
//此变量必须定义为volatile
volatile boolean initialized=false;
//假设以下代码在线程A中执行
//模拟读取配置信息,当读取完成后将initialized设置为true以通知其他线程配置可用
configOptions=new HashMap();
configText=readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized=true;
//假设以下代码在线程B中执行
//等待initialized为true,代表线程A已经把配置信息初始化完成
while(!initialized){
sleep();
}
//使用线程A中初始化好的配置信息
doSomethingWithConfig();
如果定义 initialized
变量时没有使用 volatile
修饰,就可能会由于指令重排序的优化,导致位于线程A中最后一句的代码 initialized=true
被提前执行,这样在线程B中使用配置信息的代码就可能出现错误,而 volatile
关键字则可以避免此类情况的发生。
参考自《深入理解Java虚拟机》