双重检查锁定的单例模式和延迟初始化

Posted 逆风天堂886

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了双重检查锁定的单例模式和延迟初始化相关的知识,希望对你有一定的参考价值。

  有时候需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化。此时,常用的可能就是延迟初始化,例如:懒汉式单例模式,但是要正确的实现线程安全的延迟初始化需要一些技巧,下面是非线程安全的示例代码:

public class UnsafeLazyInit {
    private static Instance instance ;
    
    public static Instance getInstance(){
        if(instance == null )             //1.A线程执行
            instance = new Instance() ;   //2.B线程执行
        
        return instance ;
    }
}

  在示例代码中,假如A线程执行步骤1的同时,B线程执行步骤2,线程A可能会看到instance引用的对象还没有初始化完成。

  我们可以对getInstance()方法做同步处理来实现线程安全的延迟初始化。示例代码如下:

public class UnsafeLazyInit {
    private static Instance instance ;
    
    public synchronized static Instance getInstance(){
        if(instance == null )            
            instance = new Instance() ;  
        
        return instance ;
    }
}

  对getInstance()方法加上了synchronized关键字进行同步处理,这将导致线程获取锁和释放锁的开销,并且线程之间竞争锁会造成阻塞。如果getInstance()方法不会被多个线程频繁调用,那么这个方案也能够提供令人满意的性能。如果需要多线程频繁的调用,将会导致线程执行性能下降。

  进一步改进,可以使用双重检查锁定来实现延迟初始化。示例代码如下:

public class UnsafeLazyInit {
    private static Instance instance ;     //1
                                           //2
    public synchronized static Instance getInstance(){  //3
        if(instance == null ){                          //4.第一次检查
            synchronized(UnsafeLazyInit.class){         //5.加锁
                if(instance ==null )                    //6.第二次检查
                    instance = new Instance();             //7.初始化对象: 问题的根源
            }
        }         
        return instance ;
    }
}

  如上代码所示,如果第一次检查结果不为null,那么就不需要进行加锁和初始化操作 。因此,可以大幅度降低synchronized带来的性能开销,看起来似乎两全其美:当多个线程试图在同一时间创建一个对象时,第5步代码通过加锁保证了只有一个线程能够创建对象。

在对象创建好之后,执行getInstance()方法将不需要再次获得锁,直接返回创建的对象。

  但是以上代码还有一个错误的优化!当线程A执行到第7步时,线程B执行到第4步,这时候线程B读取到的instance可能不为null,但是instance的引用却还没完成初始化。

  在第7步创建一个对象,可以拆分为以下三行伪代码执行:

1. memory = allocate() ;//分配对象的内存空间
2. ctorInstance(memory) ;//初始化对象
3. instance = memory ;//引用指向内存空间

  上述的伪代码,可能会被重排序,在JMM中,这种重排序是被允许的,它只保证重排序不会改变对单线程的执行结果,上述代码2、3步骤重排序不会影响单线程的执行结果,重排序之后的执行顺序如下:

1. memory = allocate() ;//分配对象的内存空间

3. instance = memory ;//引用指向内存空间
                                    //注意: 还没有初始化
2. ctorInstance(memory) ;//初始化对象

  如果是单线程访问,重排序并不会影响最后的执行结果,如下图所示:

技术分享

  下图表示多线程并发执行的情况:

 技术分享

  如上图,重排序只能保证线程A能够正确的访问对象,线程B可能访问到一个还没初始化完成的对象。

  在知晓了问题的根源之后,要实现线程安全的延迟加载,可以考虑以下两点:

  (1)不允许2和3重排序。

  (2)允许2和3重排序,但是这个重排序对其他线程不可见。

 

基于volatile的解决方案:

  只需要把以上示例的代码做一点小修改(instance声明为volatile型),就可以实现线程安全的延迟初始化。示例代码如下:

public class UnsafeLazyInit {
    private volatile static Instance instance ; 
                                           
    public synchronized static Instance getInstance(){  
        if(instance == null ){                         
            synchronized(UnsafeLazyInit.class){        
                if(instance ==null )                    
                    instance = new Instance(); //instance为volatile,会插入内存屏障,禁止重排序
            }
        }         
        return instance ;
    }
}

  注意:以上方法需要JDK5或者更高的版本,JDK5之后使用新的内存模型JSR-133内存模型,增强了volatile的内存语义,使volatile和锁拥有相同的内存语义;

 

  

以上是关于双重检查锁定的单例模式和延迟初始化的主要内容,如果未能解决你的问题,请参考以下文章

单例模式双重检查锁定与延迟初始化你不得不知道的底层原理

单例模式你不得不知道的底层原理

线程安全的单例模式

为什么双重检查锁模式需要 volatile ?

The "Double-Checked Locking is Broken" Declaration

为啥在双重检查锁定中使用volatile