volatile作用
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是可见的。
禁止进行指令重排序。
例子
1 | private boolean stop=true; |
这段Demo,给线程run1设置了一个结束标志stop,主线程等待一秒后会将介素标志stop设置为false,理论上线程run1就因为stop=false而run方法结束。但是以上代码运行并没有结束。说明stop的修改对线程run1是不可见的。
实际上是因为stop字段没有用volatile修饰,JIT编译器并不知道它是多线程共享的变量。所以为了提高代码效率,会对代码进行优化成等同以下结果:
run方法优化1
2
3while(true){
//处理业务
}
所以run1线程一直循环。如果用volatile修饰stop变量即可,解决。
原理
内存屏障
内存屏障(Memory Barrier)是一种CPU指令,内存屏障用于插入两个指令之间使用,其作用是禁止重排序和刷新处理器缓存和冲刷处理器缓存来保证可见性。Java编译器也会根据内存屏障的规则禁止重排序。
JMM种定义了这四类存储屏障
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。保障了屏障前的所有内存访问指令(存储和装载)完成之后,才执行改屏障之后的内存访问指令。
总结:java内存屏障由store和load两两组合,store和load的拼接顺序也表示着这个内存屏障的功能。例如LoadStore,前面的读操作Load会在后面的写操作Store执行前执行完毕。
执行完毕,意味中处理结果对后面的操作可见。
volatile中的内存屏障
读操作1
2
3
4
5volatile读操作
⬇
LoadLoad
⬇
LoadStore
volatile变量读操作后加入LoadLoad和LoadStore,保障volatile变量读操作后的所有普通读写操作都不能和volatile读操作重排序。
写操作1
2
3
4
5StoreStore
⬇
volatile写操作
⬇
StoreLoad
volatile变量写操作之前加StoreStore屏障,禁止上面的普通写操作不能和volatile写操作重排序
volatile变量写操作之后加StoreLoad屏障,防止上面的volatile写和下面可能存在的volatile读/写重排序
JSR-133对volatile语言进行了增强。旧内存模型允许volatile变量与普通变量重排序。JSR-133后,只要volatile变量
与普通变量之间的重排序有可能破坏volatile的内存语义,这种重排序就会被禁止。
使用场景
- 作为状态变量。如上面Demo,用于线程结束的标志。可以通知线程结束。
- 某些场景代替锁。如多个线程共享一组可变变量,要保证这些变量更新的原子性。可以将这些变量封装成对象,用volatile修饰。对这些变量更新操作可以是创建一个对象并赋予引用,volatile保证了可见性和有序性。
Brian Goetz大神的《正确使用 Volatile 变量》
详细介绍了volatile的应用场景