一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
-
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
-
禁止进行指令重排序。
例子
private boolean stop=true;
@Test
public void test() throws Exception{
Runnable run1 = new Runnable(){
public void run() {
// TODO Auto-generated method stub
while(stop){
//处理业务
}
System.out.println("2......end");
}
};
Thread thread=new Thread(run1);
thread.start();
Thread.sleep(1000);
stop=false;
System.out.println("1......end");
//等待程序运行
thread.join();
}
这段Demo,给线程run1设置了一个结束标志stop,主线程等待一秒后会将介素标志stop设置为false,理论上线程run1就因为stop=false而run方法结束。但是以上代码运行并没有结束。说明stop的修改对线程run1是不可见的。
实际上是因为stop字段没有用volatile修饰,JIT编译器并不知道它是多线程共享的变量。所以为了提高代码效率,会对代码进行优化成等同以下结果:
run方法优化
while(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屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
总结:java内存屏障由store和load两两组合,store和load的拼接顺序也表示者这个内存屏障的功能。例如LoadStore,前面的操作Load会在后面的Store执行前执行完毕。
volatile种的内存屏障
读操作
volatile读操作
⬇
LoadLoad
⬇
LoadStore
volatile变量读操作后加入LoadLoad和LoadStore,保障volatile变量读操作后的所有普通读写操作都不能和volatile读操作重排序。
写操作
StoreStore
⬇
volatile写操作
⬇
StoreLoad
volatile变量写操作之前加StoreStore屏障,禁止上面的普通写操作不能和volatile写操作重排序
volatile变量写操作之后加StoreLoad屏障,防止上面的volatile写和下面可能存在的volatile读/写重排序
使用场景
- 作为状态变量。如上面Demo,用于线程结束的标志。可以通知线程结束。
- 某些场景代替锁。如多个线程共享一组可变变量,要保证这些变量更新的原子性。可以将这些变量封装成对象,用volatile修饰。对这些变量更新操作可以是创建一个对象并赋予引用,volatile保证了可见性和有序性。