您的位置:首页 > 编程语言 > Java开发

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

2017-02-11 19:39 246 查看
参考书籍:

java并发编程的艺术

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

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

有可能出现的问题:

两个线程同时进入代码2,可能两个线程返回了两个不同的instance实例引用,如果两个线程后续还会对instance实例对象中的域进行修改操作,可能会出现数据不一致等错误。

假设线程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的双重检查锁定方案:比较繁琐,但是可以对实例字段实现延迟初始化。

基于类初始化的方案:简单,但是只能对静态字段进行延迟初始化。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息