并发编程中大家最熟悉的应该是volatile和synchronized了,使用最多的场景应该是单例吧。看代码,你觉得下边这个单例有问题吗? 图1:
public class UserManager{
private static UserManager sUserManager;
private UserManager(){}
public static UserManager getInstance(){
if(sUserManager == null){//1
synchronized (UserManager.class){
if(sUserManager == null){
sUserManager = new UserManager();
}
}
}
return sUserManager;
}
}
乍一看好像没什么毛病,有点经验的人仔细再一看,诶发现少了一个volatile。标准的写法好像应该长下边这样:
图2:
public class UserManager{
private static volatile UserManager sUserManager;
private UserManager(){}
public static UserManager getInstance(){
if(sUserManager == null){//2
synchronized (UserManager.class){
if(sUserManager == null){
sUserManager = new UserManager();// 3
}
}
}
return sUserManager;
}
}
那问题就来了,加这个volatile有什么用?不是已经加了synchronized了吗?不加不行吗? 看过很多文章,模糊记得volatile是用来保证可见性的。
什么是可见性?
一个线程写共享变量,在另外一个线程中能立即可见。比如图2(在volatile关键字下)线程A执行完3位置的代码后,这个时候线程B开始进行2位置的条件判断,就能立马看见sUserManager的值不为null。如果如图1(没有volatile加持),并不能保证这种可见性。那你可能会想synchronized难道不保证可见性吗?答案是:正确使用synchronized同步是可以保证可见性的。
如下图3:
public class UserManager{
private static UserManager sUserManager;
private UserManager(){}
public synchronized static UserManager getInstance(){//4
if(sUserManager == null){
sUserManager = new UserManager();
}
return sUserManager;
}
}
所有共享变量的访问必须由synchronized关键字同步。 同一个对象上,synchronized代码块能保证访问共享变量,是其他线程离开synchronized同步代码的操作结果,也就保证了可见性。 图1中1位置代码并没有synchronized同步,那么访问这个地方的代码就可能是失效的。图3中对sUserManager变量进行了正确的同步。
代码重排序
其实可见性并不是图1单例写法的症结。图1问题重点在于访问到位置1代码的时候,sUserManager不为null,但是UserManager还没有初始化完成。出现这种情况在于编译优化的时候代码可能会重排序。
如图4
public class UserManager{
private static UserManager sUserManager;
private String name;//6
private int age;//7
private UserManager(){
name = "聪聪";
age = 1;
}
public static UserManager getInstance(){
if(sUserManager == null){//8
synchronized (UserManager.class){
if(sUserManager == null){
sUserManager = new UserManager();// 5
}
}
}
return sUserManager;
}
}
线程A执行位置5代码后,按照直觉,应该位置6位置7代码已经执行完毕。事实上,经过编译优化后,线程A执行过位置5代码后,线程B在代码8位置处可能看到了sUserManager不为null,但是name和age还没初始化,这个时候sUserManager是个无效的值。1> 由于volatile能够禁止对共享变量的代码重排,所以图2中的单例是正确的。2> 正确的同步(所有共享变量的访问必须由synchronized关键字同步。)并不能禁止代码重排,但是能防止访问到失效的值,所以图3是正确的。