【死磕Java并发】—–Java内存模型之从JMM角度分析DCL

原文出处:http://cmsblogs.com/chenssy


DCL,即Double Check Lock,中卫双重检查锁定。其实DCL很多人在单例模式中用过,LZ面试人的时候也要他们写过,但是有很多人都会写错。他们为什么会写错呢?其错误根源在哪里?有什么解决方案?下面就随LZ一起来分析

问题分析

我们先看单例模式里面的懒汉式:

public class Singleton {
   private static Singleton singleton;

   private Singleton(){}

   public static Singleton getInstance(){
       if(singleton == null){
           singleton = new Singleton();
       }

       return singleton;
   }
}

我们都知道这种写法是错误的,因为它无法保证线程的安全性。优化如下:

public class Singleton {
   private static Singleton singleton;

   private Singleton(){}

   public static synchronized Singleton getInstance(){
       if(singleton == null){
           singleton = new Singleton();
       }

       return singleton;
   }
}

优化非常简单,就是在getInstance方法上面做了同步,但是synchronized就会导致这个方法比较低效,导致程序性能下降,那么怎么解决呢?聪明的人们想到了双重检查 DCL:

public class Singleton {
   private static Singleton singleton;

   private Singleton(){}

   public static Singleton getInstance(){
       if(singleton == null){                              // 1
           synchronized (Singleton.class){                 // 2
               if(singleton == null){                      // 3
                   singleton = new Singleton();            // 4
               }
           }
       }
       return singleton;
   }
}

就如上面所示,这个代码看起来很完美,理由如下:

  • 如果检查第一个singleton不为null,则不需要执行下面的加锁动作,极大提高了程序的性能;
  • 如果第一个singleton为null,即使有多个线程同一时间判断,但是由于synchronized的存在,只会有一个线程能够创建对象;
  • 当第一个获取锁的线程创建完成后singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象;

通过上面的分析,DCL看起确实是非常完美,但是可以明确地告诉你,这个错误的。上面的逻辑确实是没有问题,分析也对,但是就是有问题,那么问题出在哪里呢?在回答这个问题之前,我们先来复习一下创建对象过程,实例化一个对象要分为三个步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将内存空间的地址赋值给对应的引用

但是由于重排序的缘故,步骤2、3可能会发生重排序,其过程如下:

  1. 分配内存空间
  2. 将内存空间的地址赋值给对应的引用
  3. 初始化对象

如果2、3发生了重排序就会导致第二个判断会出错,singleton != null,但是它其实仅仅只是一个地址而已,此时对象还没有被初始化,所以return的singleton对象是一个没有被初始化的对象,如下:

按照上面图例所示,线程B访问的是一个没有被初始化的singleton对象。

通过上面的阐述,我们可以判断DCL的错误根源在于步骤4:

singleton = new Singleton();

知道问题根源所在,那么怎么解决呢?有两个解决办法:

  1. 不允许初始化阶段步骤2 、3发生重排序。
  2. 允许初始化阶段步骤2 、3发生重排序,但是不允许其他线程“看到”这个重排序。

解决方案

解决方案依据上面两个解决办法即可。

基于volatile解决方案

对于上面的DCL其实只需要做一点点修改即可:将变量singleton生命为volatile即可:

public class Singleton {
   //通过volatile关键字来确保安全
   private volatile static Singleton singleton;

   private Singleton(){}

   public static Singleton getInstance(){
       if(singleton == null){
           synchronized (Singleton.class){
               if(singleton == null){
                   singleton = new Singleton();
               }
           }
       }
       return singleton;
   }
}

当singleton声明为volatile后,步骤2、步骤3就不会被重排序了,也就可以解决上面那问题了。

基于类初始化的解决方案

该解决方案的根本就在于:利用classloder的机制来保证初始化instance时只有一个线程。JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化

public class Singleton {
   private static class SingletonHolder{
       public static Singleton singleton = new Singleton();
   }

   public static Singleton getInstance(){
       return SingletonHolder.singleton;
   }
}

这种解决方案的实质是:运行步骤2和步骤3重排序,但是不允许其他线程看见。

Java语言规定,对于每一个类或者接口C,都有一个唯一的初始化锁LC与之相对应。从C到LC的映射,由JVM的具体实现去自由实现。JVM在类初始化阶段期间会获取这个初始化锁,并且每一个线程至少获取一次锁来确保这个类已经被初始化过了。

参考资料

  1. 方腾飞:《Java并发编程的艺术》
赞(22) 打赏

如未加特殊说明,此网站文章均为原创,转载必须注明出处。Java 技术驿站 » 【死磕Java并发】—–Java内存模型之从JMM角度分析DCL
分享到: 更多 (0)

评论 27

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #8

    说一下我自己的看法,首先,synchronized不是可以保证有序性吗,为什么synchronized修饰的代码块中singleton初始化还会出现指令重排呢。 博主之前的文章说过,指令重排有一项就是在输出结构是一样的情况下,指令重排也是合法的,对于线程1进入synchronized块中进行singleton的实例化时,指令重排对它自己是没有影响的,因为它占用后不管怎么重排结果都是实例化完成。但是对于线程2来说,按博主说的情况,有可能线程1只是将地址空间赋值给引用,还没进行初始化,则线程2拿到的就只是应用,因此需要加上volatile来禁止重排序。当然这些只是可能,但是不记得哪位大佬说的了,在高并发的情况下,任何可能都会成真,所以还是老老实实加上volatile,final也行

    lovexl1个月前 (05-12)回复
    • 总结的很好

      chenssy1个月前 (05-13)回复
  2. #7

    讲得清楚明白!感谢!

    发给官兵3个月前 (03-18)回复
    • 不客气,感谢关注

      chenssy3个月前 (03-18)回复
  3. #6

    实例化的步骤2、3真的会发生重排序吗

    fq6个月前 (01-09)回复
    • 嗯嗯!!

      chenssy6个月前 (01-09)回复
      • 第二步的初始化是堆内存的数据初始化,应该是对所有线程可见,不涉及cpu缓存啊,后来的线程得到第三步完成的通知时第二步照理说执行完了吧

        fq6个月前 (01-10)回复
      • 我自己测了一下,x64的cpu,第一个线程创建一个大对象实例(花费30ms),第二个线程睡10ms后去尝试获取。也没有发生过初始化之前就获取到对象的情况

        fq6个月前 (01-10)回复
        • 这种情况很难测试,你可以看看 ArrayBlockingQueue 这个阻塞队列,里面的思想都都差不多,我专门写了一篇博客分析了:http://cmsblogs.com/?p=2458

          chenssy6个月前 (01-10)回复
          • 这个理论确实可以解释很多问题,但是我还是有一些想不通的地方
            1、volatile这个关键字如果只是靠插入内存屏障防止重排序,那应该解决的只是CPU缓存行对于多线程的可见性问题而已,我看大神你前面的文章有提过还有编译器级别的重排序,这个是怎么处理的。
            2、堆内存不是线程共享么,对堆里面的对象进行初始化,别的线程会看不见吗

            fq6个月前 (01-10)
          • 这个我还没有研究这么深

            chenssy6个月前 (01-10)
          • 第二个问题当我没说。。。是我脑子进水了。但是第一个还是想不通

            fq6个月前 (01-10)
  4. #5

    请问缘何要用 内部类 包一层?

    hj6个月前 (12-26)回复
    • 这个我也不理解,完全可以不用内部类包一层,直接private static Singleton singleton = new Singleton();这样也是类的初始化解决方案

      df3个月前 (03-19)回复
      • 因为你这种写法是饿汉式,他虽然也不存在线程安全问题,但是它采用的方式是空间换时间

        chenssy3个月前 (03-19)回复
      • 饿汉式是在单利一初始化的时候就会开辟一块内存来装载,如果我一直都不调用方法他在内存中也会存在这么一块空间,但是使用内部类就不一样了,他并不是随着单例类初始化的而是调用 getInstance() 方法的时候才会进行初始化工作。

        chenssy3个月前 (03-19)回复
  5. #4

    话说 private static final 修饰能否保证双重检验单例的正确性呢?

    a12583283007个月前 (12-05)回复
    • 可以

      chenssy7个月前 (12-05)回复
  6. #3

    楼主,在实例化对象的时候,虽然分了三个步骤有可能会发生重排序,但是这三个步骤是被封装到synchronized中,那么线程A没执行完这三个步骤是没有释放同步锁的。那么线程B根本就进不来这个同步代码块。又谈什么获取到没有初始化完成的对象? 我是个菜鸟,真的有点不理解,希望你能帮我解答

    梦里花h2年前 (2017-07-21)回复
    • 第一个Check并没有放入到sychronized中!

      chenssy2年前 (2017-07-24)回复
  7. #2

    厉害了。简直颠覆了我的世界观

    沧海一屌丝2年前 (2017-06-19)回复
    • 谢谢支持!!

      chenssy2年前 (2017-06-20)回复
  8. #1

    文中说;如果2、3发生了重排序就会导致第二个判断会出错,这里应该是第一个判断吧,第二个判断已经被锁住了,B线程 不可能进入第二个判断呀

    灰尘跳动2年前 (2017-03-03)回复
    • 是不会进入啊!但是不为null了,并不代表对象已经完成了实例化过程啊!所以第二个线程判断singleton == null为false的时候,return singleton 说不定是假的呢?第一个线程其实还没有完成实例化过程

      selfImpro2年前 (2017-03-03)回复
      • 我认为对象只有完成了实例化过程,才会跳出synchronized 的代码块,这里加volatile的原因是因为:当线程A进行实例化的时候发生了重排序,使得内存地址先被引用,而在这时,线程B进行了第一次判断(不在synchronized 代码块中的判断),这时singleton == null为false,直接return

        灰尘跳动2年前 (2017-03-03)回复
        • 实例化是分为三步的。三个步骤是可以重排序的,具体情况请参考博文!你分析的没有错是直接return,但是呢?仅仅只是占用了地址而已,对象在第二个线程中还是没有初始化完成啊

          selfImpro2年前 (2017-03-03)回复

关注【Java 技术驿站】公众号,每天早上 8:10 为你推送一篇技术文章

扫描二维码关注我!


关注【Java 技术驿站】公众号 回复 “VIP”,获取 VIP 地址永久关闭弹出窗口

免费获取资源

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏