java多线程之延迟初始化

有时候我们可能推迟一些高开销的对象的初始化操作,并且只有在使用这些对象时才进行初始化,开发者可以采用延迟初始化来实现该需求。但是要正确实现线程安全的延迟初始化还是需要一些技巧的,否则很容易出现问题。下面是一个非线程安全的延迟初始化的例子:

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

}
在UnsafeLazyInit类中,假设A线程执行代码1的同时,B线程执行代码2,此时线程A可能会看到instance对象还没有完成初始化。
对于UnsafeLazyInit类,我们可以对getInstance方法做同步处理来实现线程安全的延迟初始化,代码如下:

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

}
由于对getInstance方法做了同步处理,将导致性能开销,如果getInstance方法被多个线程频繁调用的话,将会导致程序执行性能的下降,而如果getInstance不会被多个线程频繁调用,那么这个方案将会提供令人满意的性能。
对于synchronized方法可能带来的程序执行性能的下降,我们可以使用一种“聪明”的技巧:双重检查锁定(Double-Checked Locking)来降低同步的开销。下面是使用双重检查锁来实现延迟初始化的示例代码:
public class DoubleCheckedLocking {

	private static Instance instance;
	
	public static Instance getInstance() {
		if(instance == null) {                              //1:第一次检查
			synchronized(DoubleCheckedLocking.class) {      //2:加锁
				if(instance == null) {                      //3:第二次检查
					instance = new Instance();              //4:问题的根源处在这里
				}
			}
		}
		return instance;
	}

}
按照上面的代码,如果第一次检查instance不为null,则不需要执行下面的加锁和二次检查与初始化操作,因此可以大大降低synchronized带来的性能开销,似乎是两全其美的实现方式。
双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行1:第一次检查时,代码读取到instance不为null,其实instance有肯能还没有完成初始化,该问题的根源就在于:重排序。
在创建instance实例时,instance = new Instance()这行代码可以分解为如下3行伪代码:

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

上述伪代码中的2和3之间,可能会发生重排序,重排序后的执行顺序如下:

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

在上边的java代码中,如果instance = new Instance()发生了重排序,另一个并发线程B就有可能在第一次检查时instance不为null,线程B接下来将访问instance所引用的对象,但此时该对象可能还没有被A线程初始化,也就是会访问一个未被初始化的对象。
知道了这个问题根源以后,可以有两个办法来实现线程安全的延迟初始化:
1.不允许2和3重排序。
2.允许2和3重排序,但不允许其它线程“看到”这个重排序。

1.不允许2和3重排序,只需对双重检查锁定做小小的修改即可,我们把instance声明为volatile型,就可以实现线程安全的延迟初始化,示例代码如下:

public class DoubleCheckedLocking {

	private volatile static Instance instance;
	
	public static Instance getInstance() {
		if(instance == null) {
			synchronized(DoubleCheckedLocking.class) {
				if(instance == null) {
					instance = new Instance();   //instance为volatile,现在没有问题了。
				}
			}
		}
		return instance;
	}

}
当对象声明为volatile后,伪代码中的2和3的重排序,在多线程环境中将被禁止。

2.允许2和3重排序,但不允许其它线程“看到”这个重排序。
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对一个类的初始化。基于这个特性,我们可以在允许2和3重排序的情况下,实现线程安全的延迟初始化。

public class InstanceFactory {
	
	private static class InstanceHolder {
		public static Instance instance = new Instance();
	}
	
	public static Instance getInstance() {
		return InstanceHolder.instance;    //这里将导致InstanceHolder类被初始化
	}
	
}
这个方案的实质是:允许2和3重排序,但是不允许非构造线程(如线程B)“看到”这个重排序。
在InstanceFactory中,首次执行getInstance方法的线程(如线程A)将导致InstanceHolder类被初始化,但是如果多个线程同时调用getInstance方法,将会怎样呢?
Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应,从C到LC的映射,由JVM的具体实现去自由实现。JVM在初始化期间会获取这个初始化锁,并且每个线程至少获取一次锁来确保这个类被初始化了。
这个过程比较冗长,这里不做过多描述,总之就是JVM通过初始化锁同步了多个线程同时初始化一个对象的操作,保证类不会被多次初始化。

通过对比基于volatile的双重检查锁定的方案和基于类初始化的方案,我们发现基于类初始化的方案更加简洁。但基于volatile的双重检查锁定方案有一个额外优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。

在设计模式中,有一个单例模式(Singleton),该模式比较常用,我们可以使用基于volatile的双重检查锁定和基于类初始化的方案去创建单例对象,在实际工作中,我一般是使用基于类初始化的方案去实现单例模式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值