5.volatile:有序性
1.什么是有序性
程序按照写代码的先后顺序执行,就是有序的。程序难道还能不按代码顺序执行?这就涉及到CPU的指令重排序问题。
2.指令重排
处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。那它如何保证即使重排最终结果也能正确呢?
答案就是保证指令数据间依赖关系不会被重排所影响。看如下例子:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
语句2 3 4之间存在相互依赖关系,所以重排的结果一定会保证2->3->4这个顺序,但是1 2顺序就不一定了。
这种重排在单线程中没有问题,但对于对线程就可能存在问题了,看如下代码:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
线程1中语句1 2之间没有依赖,可能会先执行inited=true;这将导致线程2收到错误的消息,使用还未初始化的context。不过我代码是怎么都不能重现,它总是按代码顺序执行,这就没法验证了。
我们可以为inited加上volatile关键字,这样在对inited修改前,一定会保证之前的代码全部执行完了,就不会出现重排导致的问题了。
关于指令重排还有很多内容,比如happens-before原则,内存屏障等等,要完全理解可能要深入到硬件,比较复杂和专业,再深入下去感觉要走火入魔,怕钻进去出不来,所以浅尝辄止了,大家有兴趣可以继续深入了解。
3.最佳实践
volatile禁止指令重排,最典型的应用应该就是双重锁的单例模式了。看代码
public class SecondSingleton {
//volatile关键字保证可见性 同时禁用指令重排(jdk1.5后生效)
private static volatile SecondSingleton singleton;
private SecondSingleton(){
}
/**
* 双重检查锁实现单例模式
* 推荐这样写,既保证线程安全,又延迟加载
* @return
*/
public static SecondSingleton getSingleton() {
if (singleton == null) {
synchronized (SecondSingleton.class) {
if(singleton == null){
singleton = new SecondSingleton();
}
}
}
return singleton;
}
}
为什么要使用volatile修饰singleton?
主要在于singleton = new SecondSingleton();这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:
- 给 singleton 分配内存
- 调用 SecondSingleton的构造函数来初始化成员变量
- 将singleton 对象指向分配的内存空间(执行完这步 singleton 就为非 null 了)
但是由于指令重排,上述第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1->2->3 也可能是 1->3->2。如果是后者,则在 3 执行完毕,2 未执行之前,另一个线程执行if (singleton == null) ,这时 singleton 已经是非 null 了,但却没有初始化,所以该线程会直接返回 singleton ,然后使用,自然就会报错了。