并发基础-单例中的volatile

315 阅读3分钟

并发编程中大家最熟悉的应该是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是正确的。