Double-Check中的volatile作用

转载自【张新强】的博客

看似完美的单例模式?

public class Single {
    private static Single3 instance;
    private Single() {}
    public static Single getInstance() {
        if (instance == null) {
            synchronized (Single.class) {
                if (instance == null) {
                    instance = new Single3();
                }
            }
        }
        return instance;
    }
}
  • 第一个if (instance == null),其实是为了解决效率问题,只有instance为null的时候,才进入synchronized的代码段——大大减少了几率。
  • 第二个if (instance == null),则是为了防止可能出现多个实例的情况。

那么还会有问题吗?

答:只是『看起来』,还是有小概率出现问题的。

原子操作

原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度打断的操作。

例子:

m = 6; // 这是个原子操作

假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。

而,声明并赋值就不是一个原子操作:

int n = 6; // 这不是一个原子操作

对于这个语句,至少有两个操作:
① 明一个变量n
② 给n赋值为6
——这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。
——这样,在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能会导致不稳定的结果出现。

指令重排

概念

简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。

例子:

int a ;   // 语句1 
a = 8 ;   // 语句2
int b = 9 ;     // 语句3
int c = a + b ; // 语句4

正常来说,对于顺序结构,执行的顺序是自上到下,也即1234
但是,由于指令重排的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324
由于语句34没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排
——也就是说,

对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序

回到话题

主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

  1. 给 singleton 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
  3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

错误根源:

再稍微解释一下,就是说,由于有一个『instance已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (instance == null)这里,这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。这里的关键在于——线程T1对instance的写操作没有完成,线程T2就执行了读操作

完全版

public class Single {
    private static volatile Single4 instance;
    private Single() {}
    public static Single getInstance() {
        if (instance == null) {
            synchronized (Single.class) {
                if (instance == null) {
                    instance = new Single();
                }
            }
        }
        return instance;
    }
}

volatile发挥的作用

volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障(什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。

注意:volatile阻止的不是singleton = new Singleton()这句话内部[1-2-3]的指令重排而是保证了在一个写操作([1-2-3])完成之前不会调用读操作(if (instance == null))。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • volatile关键字经常在并发编程中使用,其特性是保证可见性以及有序性,但是关于volatile的使用仍然要小心...
    Ruheng阅读 13,378评论 40 134
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,473评论 11 349
  • 有个国度叫快速国,那的一天只有三个小时,吃饭爸爸用一分半,妈妈用一分四十,儿子用两分钟。他们每分钟的心跳是200下...
    晨光微晓阅读 3,012评论 0 0
  • APP的数据指标体系主要分为五个维度,包括用户规模与质量、参与度分析、渠道分析、功能分析以用户属性分析。用户规模和...
    lj神经刀阅读 5,036评论 0 12
  • 好多天没写了,最近迷上了麻将,天天忙着顶。不出错是最好的
    宋世巍阅读 847评论 0 0