并发基础-单例中的volatile

并发编程中你们最熟悉的应该是volatile和synchronized了,使用最多的场景应该是单例吧。看代码,你以为下边这个单例有问题吗? 图1:java

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变量进行了正确的同步。spa

代码重排序

其实可见性并非图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是正确的。code

相关文章
相关标签/搜索