线程安全的延迟初始化方式

参考书籍:

java并发编程的艺术

在java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化。因此需要引入延迟初始化技术。

在单线程中延迟初始化对象的代码,在多线程中是非线程安全的,因为此时会出现竞态条件(Race Condition),典型的先判断后执行。

有可能出现的问题:

  1. 两个线程同时进入代码2,可能两个线程返回了两个不同的instance实例引用,如果两个线程后续还会对instance实例对象中的域进行修改操作,可能会出现数据不一致等错误。
  2. 假设线程A执行代码1的同时,B线程执行代码2,此时,线程A可能会看到instance引用的对象还没有完成初始化,也就是返回了一个不完整的实例对象引用。
public class UnsafeLazyInitialization{
    private static Instance instance;
    public static Instance getInstance(){
        if(instance == null)//1
            instance = new Instance();//2
        return instance;
    }
}

如果把instance引用声明为volatile类型的呢?这样确实可以保证instance的可见性了,但是整个方法仍不是原子的,除非能确保只有一个线程对instance进行修改操作。

为了保证整体方法的原子性,可以在方法上进行同步

public class SafeLazyInitialization{
    private static Instance instance;
    public synchronized static Instance getInstance(){
        if(instance == null)//1
            instance = new Instance();//2
        return instance;
    }
}

由于对整个方法做了同步处理,将会产生额外的性能开销。如果getInstance()方法被多个线程频繁的调用,将会导致程序性能的下降。反之,如果getInstance()方法不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。

在早期的jvm中,synchronized(甚至是无竞争的synchronized)存在巨大的性能开销。因此人们通过双重检查锁定(Double-Checked Locking DCL )来降低同步的开销。

因为只有在第一次初始化instance时需要同步,因此第一次代码1的检查不加锁;之所以需要在同步代码块中第二次检查,是因为两个线程有可能同时通过代码1往下执行,当线程A获得锁执行完成同步代码块的代码后(此时instance已经初始化完成了),释放锁,然后线程B获得锁进入同步代码块,如果不再次对instance进行null值检查,那么将错误地初始化两次instance!因此第二次检查是必要的(由于synchronized能保证代码块中变量的可见性,因此线程B能够检测到线程A对instance的修改)。

public class DoubleCheckedInitialization{
    private static Instance instance;
    public static Instance getInstance(){
        if(instance == null){//1
            synchronized(DoubleCheckedInitialization.class{
                if(instance == null){//2
                    instance = new Instance();//3
                }
            }
        }
        return instance;
    }
}

看起来很完美了,但是还是存在缺陷:代码3在jvm中执行使,可以分解为如下的3行代码:

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

其中代码b和c可能会被重排序。因此,可能会出现以下情境:

线程A进入代码2第二个检查,开始初始化对象,由于指令重排序,abc执行顺序变为了acb,当执行完代码a和c时,instance已经被赋值,不为空,此时线程B调用getInstance()方法,进入代码1第一个检查,发现不为空,直接返回了instance的引用。因此线程B将会访问到一个还未初始化的对象(或者未初始化完成)。也就是说对象未被正确地发布。

因此为了禁止代码b和c指令重排序,需要将instance引用声明为volatile类型。注意,声明volatile是为了禁止指令重排序,不是为了保证可见性。

public class DoubleCheckedInitialization{
    private volatile static Instance instance;
    public synchronized static Instance getInstance(){
        if(instance == null){//1
            synchronized(DoubleCheckedInitialization.class{
                if(instance == null){//2
                    instance = new Instance();//3
                }
            }
        }
        return instance;
    }
}

一个典型的双重锁检查方案的实际应用:sfj4j初始化LoggerFactory

/**
 * Return the {@link ILoggerFactory} instance in use.
 * <p/>
 * <p/>
 * ILoggerFactory instance is bound with this class at compile time.
 * 
 * @return the ILoggerFactory instance in use
 */
public static ILoggerFactory getILoggerFactory() {
    if (INITIALIZATION_STATE == UNINITIALIZED) {
        synchronized (LoggerFactory.class) {
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                performInitialization();
            }
        }
    }
...

另外还有一种类初始化的解决方案:

public class InstanceFactory{
    private static class InstanceHolder{
        public static Instance instance = new Instance();
    }

    public static Instance getInstance(){
        return InstanceHolder.instance;
    }
}

volatile的双重检查锁定方案:比较繁琐,但是可以对实例字段实现延迟初始化。
基于类初始化的方案:简单,但是只能对静态字段进行延迟初始化。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值