Java多线程并发中的双重检查锁定与延迟初始化

来源:互联网 发布:sql语句添加列 编辑:程序博客网 时间:2024/05/19 05:38

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

在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。双重检查锁定是常见的延迟初始化技术。
下面我们看一个非线程安全的延迟初始化对象的例子:

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

假设A线程执行代码1的同时,B线程执行代码2。此时,线程A可能会看到instance引用的对象还没有完成初始化
下面我们对上面的代码改造一下,让他变成线程安全

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

由于对getInstance()方法做了同步处理,synchronized将导致性能开销。如果getInstance()方法被多个线程频繁的调用,将会导致程序执行性能的下降(一般情况在我们项目中提供的单例总是被频繁的调用)。反之,如果getInstance()方法不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。
下面我们进一步通过双重检查锁定来降低同步的开销,代码如下:

public class Singleton {    private static Singleton instance;    public static Singleton getInstance() {        // 第一次检查        if(instance == null){            // 加锁            synchronized(Singleton.class){                if (instance == null)                     //分配内存空间、初始化对象、instance指向分配的内存地址                    instance = new Singleton();             }        }        return instance;    }}

上面的代码真的能保证单例吗?让我们来分析下
如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美。多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。在对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回已创建好的对象。双重检查锁定看起来似乎很完美,但这是一个错误的优化!在代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。
让我们来继续分析
前面的双重检查锁定示例代码(instance=new Singleton();)创建了一个对象。这一行代码可以分解为如下的3行伪代码。

memory = allocate();  // 1:分配对象的内存空间ctorInstance(memory); // 2:初始化对象instance = memory;  // 3:设置instance指向刚分配的内存地址

面3行伪代码中的2和3之间,可能会被重排序2和3之间重排序之后的执行时序如下。

memory = allocate();  // 1:分配对象的内存空间instance = memory;  // 3:设置instance指向刚分配的内存地址;注意,此时对象还没有被初始化!ctorInstance(memory); // 2:初始化对象

下面让我们看一下多线程并发的执行情况

这里写图片描述
这里写图片描述

由于单线程内要遵守intra-thread semantics,从而能保证A线程的执行结果不会被改变。但是,当线程A和B按图3-38的时序执行时,B线程将看到一个还没有被初始化的对象。
基于上面的问题现象我们有2种解决方案
1、不允许2和3重排序
2、允许2和3重排序,但是不允许对其他线程“看到”这个重排序
方案一:基于volatile解决方案

只需要前面基于双重检查锁定来实现的延迟方案,把instance改成volatile(JDK1.5以上支持)

public class Singleton {    private static volatile Singleton instance;    public static Singleton getInstance() {        // 第一次检查        if(instance == null){            // 加锁            synchronized(Singleton.class){                if (instance == null)                     //分配内存空间、初始化对象、instance指向分配的内存地址                    instance = new Singleton();             }        }        return instance;    }}

当声明为volatile以后2和3重排将被禁止,代码将按照如下顺序执行执行
这里写图片描述

方案二:基于类初始化的解决方案
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案

public class Singleton {    private static class InstanceHolder {        public static Singleton instance = new Singleton();    }    public static Singleton getInstance() {        // 这里将导致InstanceHolder类被初始化        return InstanceHolder.instance;    }}

假设两个线程并发执行getInstance()方法,下面是执行的示意图

这里写图片描述