『每日一问』怎么实现一个正确的双重检查锁定

『每日系列』

缘由

我们程序里,有时候一些开销比较大的对象创建,往往不会提前创建,而是只有在实际要用到的时候才会去创建。
也就是基本下边这个写法:

public class UnsafeLazyInitialization {
	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还没有完成初始化。那么就产生的内存可见性问题。

劳动人民的智慧是无穷的 “加个synchronized完事了”~~

public class UnsafeLazyInitialization {
	private static Instance instance;
	public synchronized static Instance getInstance() {
		if (instance == null)          
			instance = new Instance(); 
		return instance;
	}
}

但是这个写法吧,如果并发线程很多,那么锁的频繁竞争就会导致性能开销很大。因为在早期的JVM中,synchronized性能比较差,所以劳动人民的智慧又发出了炽热的光芒闪瞎了JVM的狗眼~~

双重检查锁定(Double-Checked Locking)横空出世!!!

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

按照上边的这个写法,在第一次检查的时候,如果对象不为null,那么就直接返回对象实例。如果对象为null,那么就加锁去实例化对象。也就保证了多线程下只有一个线程可以去实例化对象。

看起来很完美,但其实还是老问题,线程A在判断instance不为null之后获取到的instance可能还没有初始化完成。

那么为什么会出现这种情况呢?

根源

一个对象的创建可以大概概括为三个步骤: TODO:不仅仅是这三步,后面会出文章专门补充

  1. 分配内存空间
  2. 初始化对象
  3. 设置对象指向分配的内存空间

问题就出在上面的第二步和第三步可能会出现重排序。

Java语言规范[The Java Language Specification]中要求,所有线程在执行Java程序时必须遵守intra-thread semantics(线程内语义),intra-thread semantics允许那些在单线程内不会改变执行结果的重排序。为啥要允许呢,因为可以提高程序的执行性能呗~

Mon 06 Mon 13 Mon 20 A1:分配对象的内存空间 A3:设置对象指向内存空间 B1:判断对象是否为nul B2:不为null,访问对象 A2:初始化对象 A4:访问对象 t1 t2 t3 t4 t5 t6 一个会导致此bug出现的逻辑执行顺序

上述时序图中,A2和A3虽然重排序了,但是按照Java内存模型的intra-thread semantics将确保A4排在A2前面。只有这样A线程的执行结果才不会改变。但是因为A2、A3的重排将导致B1在判断instance是为为null的时候返回false,因为这个时候A3已经执行完了。所以呢,B2直接访问对象的时候就会出问题了,毕竟这个对象还没有初始化完毕呢。

解决方案

知道了问题产生的原因,那么相应的解决思路也就很顺滑的出来了是吧

  1. 禁止A2、A3重排
  2. A2、A3重排的时候,对别的线程不可见
public class DoubleCheckedLocking { 	                // 1
	private volatile static Instance instance;	        // 2 : 关键字volatile
	public static Instance getInstance() {              // 3
		if (instance == null) {                         // 4  ---  线程A
			synchronized (DoubleCheckedLocking.class) { // 5
				if (instance == null)                   // 6
					instance = new Instance();          // 7  ---  线程B
			}                                          
		}                                               
		return instance;                                
	}                                                   
}

第一种思路的可以使用关键字volatile来实现。『每日一问』volatile干嘛的
通过volatile的内存语义我们可以知道,在线程A执行到第四行的时候,如果线程B在第七行的对象创建没有完成,则线程B不会刷新主内存,那么线程A就不会出现instance的内存可见性问题。
······································································

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

第二种思路可以借助类初始化的思路去实现。
每一个类在初始化的时候都需要去获取一个“锁[其实是对象头中的一个状态值]”,只有获取这个“锁”的线程才可以执行对象的初始化工作。这里需要说一点,一个类的静态变量被赋值,会触发类的初始化~~
右上我们可以看到,借用类初始化的特性,我们避免了一个对象在尚未初始化完成之前被别的线程使用而导致的内存可见性问题~

你可能感兴趣的:(【每日系列】,双重检查)