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

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

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和锁拥有相同的内存语义;

  

时间: 2024-11-04 01:47:02

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

双重检查锁定

看 "java并发编程的艺术" 第3.8 双重检查锁定与延迟初始化 在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销.双重检查锁定是常见的延迟初始化技术,但它是一个错误的用法.本文将分析双重检查锁定的错误根源,以及两种线程安全的延迟初始化方案. 需要注意的是, 双重检查锁定本身是错误的! 双重检查锁定的由来 在Java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化.此时,程序员可能会采用延迟初始化.但要正确实现线

双重检查锁定与延迟初始化

在Java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化.此时程序员可能会采用延迟初始化.但要正确实现线程安全的延迟初始化需要一些技巧,否则很容易出现问题.比如,下面是非线程安全的延迟初始化对象的示例代码: public class UnsafeLazyInitialization { private static Instance instance; public static Instance getInstance() { if (instanc

JAVA 双重检查锁定和延迟初始化

双重检查锁定的由来在Java程序中,有时需要推迟一些高开销的对象的初始化操作,并且只有在真正使用到这个对象的时候,才进行初始化,此时,就需要延迟初始化技术.延迟初始化的正确实现是需要一些技巧的,否则容易出现问题,下面一一介绍. 方案1 public class UnsafeLazyInit{ private static Instance instance; public static Instance getInstance(){ if (instance == null){ instance

双重检查锁定的由来

双重检查锁定的由来 在java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化.此时程序员可能会采用延迟初始化.但要正确实现线程安全的延迟初始化需要一些技巧,否则很容易出现问题.比如,下面是非线程安全的延迟初始化对象的示例代码: public class UnsafeLazyInitialization { private static Instance instance; public static Instance getInstance() { i

双重检查锁定原理详解

双重检查锁定与延迟初始化 在java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化.此时程序员可能会采用延迟初始化.但要正确实现线程安全的延迟初始化需要一些技巧,否则很容易出现问题.比如,下面是非线程安全的延迟初始化对象的示例代码: public class UnsafeLazyInitialization { private static Instance instance; public static Instance getInstance()

双重检查锁实现单例模式的线程安全问题

一.结论 双重校验锁的单例模式代码如下: public class Singleton { private static Singleton singleton; private Singleton() {} public static Singleton getSingleton() { if (singleton == null) { // 1 synchronized (Singleton.class) { // 2 if (singleton == null) { // 3 single

Singleton(单例)模式和Double-Checked Locking(双重检查锁定)模式

问题描述 现在,不管开发一个多大的系统(至少我现在的部门是这样的),都会带一个日志功能:在实际开发过程中,会专门有一个日志模块,负责写日志,由于在系统的任何地方,我们都有可能要调用日志模块中的函数,进行写日志.那么,如何构造一个日志模块的实例呢?难道,每次new一个日志模块实例,写完日志,再delete,不要告诉我你是这么干的.在C++中,可以构造一个日志模块的全局变量,那么在任何地方就都可以用了,是的,不错.但是,我所在的开发部门的C++编码规范是参照Google的编码规范的. 全局变量在项目

双重检查锁单例模式为什么要用volatile关键字?

前言 从Java内存模型出发,结合并发编程中的原子性.可见性.有序性三个角度分析volatile所起的作用,并从汇编角度大致说了volatile的原理,说明了该关键字的应用场景:在这补充一点,分析下volatile是怎么在单例模式中避免双检锁出现的问题的. 并发编程的3个条件 1.原子性:要实现原子性方式较多,可用synchronized.lock加锁,AtomicInteger等,但volatile关键字是无法保证原子性的:2.可见性:要实现可见性,也可用synchronized.lock,v

Java单例模式中双重检查锁的问题

单例创建模式是一个通用的编程习语.和多线程一起使用时,必需使用某种类型的同步.在努力创建更有效的代码时,Java 程序员们创建了双重检查锁定习语,将其和单例创建模式一起使用,从而限制同步代码量.然而,由于一些不太常见的 Java 内存模型细节的原因,并不能保证这个双重检查锁定习语有效. 它偶尔会失败,而不是总失败.此外,它失败的原因并不明显,还包含 Java 内存模型的一些隐秘细节.这些事实将导致代码失败,原因是双重检查锁定难于跟踪.在本文余下的部分里,我们将详细介绍双重检查锁定习语,从而理解它