来!梳理一下我们好像都很熟悉的单例模式

来!梳理一下我们好像都很熟悉的单例模式

public class XxxxUtil {
	
	private XxxxUtil() {
	}
	
	private static XxxxUtil instance = new XxxxUtil();
	
	public static XxxxUtil getInstance() {
		 return instance;
	}
}

 这样会有什么问题吗?
试想,有时候我们需要创建的这个instance = new XxxxUtil() 对象实例比较大,创建它很耗时间和资源,这种饿汉方式会导致,在Class加载期间就去创建这么一个很大的对象,万一应用运行期间,有可能一直用不到这个单例呢,那不白白浪费时间和资源,所以我们进一步优化的想法就是在真正需要使用的时候再去创建它,就是延迟加载,这种实现方式就叫懒汉模式
代码修改如下:

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

这样的实现,在单线程下执行是没有问题的,但是在多线程环境下又带来一个新问题,会出现重复创建多个instance实例的情况,问题场景如下:
当线程A和线程B同时执行到 if(instance == null) 时,都判断为true, 都执行了 instance = new XxxxUtil() , 先执行的线程对 instance 变量赋的值被后一个执行的线程覆盖,那么优先执行 instance = new XxxxUtil() 创建的实例 就是无用功,白白浪费系统资源。
有问题,我们就能想到与之对应的解决办法,你可能已经想到了,用 synchronized 来修饰 getInstance() 方法,以保证同一时刻只能有一个线程进入方法体.
代码修改如下:

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

这样是不是就万无一失了呢?
NO , 在多线程下,这样做虽然避免了重复创建对象的问题,但又带来了一个性能问题。

试想,当在一个并发很高的web应用下有N个线程去调用getInstance()方法,因为此方法被 Synchronized 修饰了,会导致这N个线程排队,一个一个地获取这个Instance对象实例,很是影响系统的 TPS 或 QPS 。
怎么解决这个问题呢?你可能已经想到答案了,对,就是利用双重锁检查(Double lock checking),简称DCL
代码修改如下:

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

这样以来:
1、getInstance()方法去掉了Synchronized 修饰,避免了线程排队问题;
2、避免了重复创建对象问题:
     2.1、当第一个线程调用getInstance()方法时, 会进入Synchronized 代码块来创建对象实例并赋值给instance变量 ,其他线程再调用此方法时,在“1”处发现instance变量已经有值了,将直接走"4"返回instance;
     2.2、如果当两个线程同时执行到了“2”处,肯定有一个线程优先进入Synchronized 代码块, 我们称它线程A, 另一个线程B就会在此处等待,直到线程A走出Synchronized 代码块,这时线程B进入Synchronized 代码块,在“3” 处再次判断 if(instance == null) :false , 直接走“4” 返回对象实例。

到此为止,是不是感觉我们的代码已经是无懈可击了!!!
别高兴的太早,要想写好一个单例,需要考虑的问题还不止这些,下面我们继续.......

问题就在上面代码的“5”处,这里不就是new了一下对象么,能有什么问题!!
我们把这个类XxxxUtil 的结构稍微改动一下,以便于说明问题,代码如下:

public class XxxxUtil {
	private int a;
	private int b;
	
	private XxxxUtil(int a,int b) {
		this.a = a; // 6
		this.b = b; // 7
	}
	
	private static XxxxUtil instance = null; // 8
	
	public static XxxxUtil getInstance() {
		 if(instance == null) { // 1
			 synchronized(XxxxUtil.class) { // 2
				 if(instance == null) { // 3
					 instance = new XxxxUtil(2,3); // 5
				 }
			 }
		 }
		 return instance; // 4
	}
}

我们给这个类的构造过程增加点逻辑:分别为两个进行变量赋值。 
我们再来理解一下“5”处instance = new XxxxUtil(2,3); // 5,这个对象创建的大概步骤:

  1. 创建对象
  2. 初始化对象的各个域(int a, int b),为其赋值
  3. 将对象指向其引用instance 

CPU为了提高执行效率,会对以上三个步骤进行重排序,试想如果上面的”第3步先被执行了,对于上面这个单例的实现会有什么影响?
假设有两个线程A、线程B,考虑如下场景:
线程A首次调用 getInstance() 执行到了“5”处,按如上所说,此时 instance = new XxxxUtil(2,3) 的 “第3步” 先被行了,且还未执行”第1,2步“,此时,线程B调用 getInstance() 执行到”1“处,判断 if(instance == null) :false  ,直接走“4”返回了一个未创建完整的对象实例,哇!如果是生产环境.........好恐怖!!!!!  

解决以上问题的关键在于:怎么阻止 instance = new XxxxUtil(2,3); 创建对象三个步骤的重新排序
解决办法是:在”8“处用volatile修饰变量instance , 代码调整如下:

public class XxxxUtil {
	private int a;
	private int b;
	
	private XxxxUtil(int a,int b) {
		this.a = a; // 6
		this.b = b; // 7
	}
	
	private static volatile XxxxUtil instance = null; // 8
	
	public static XxxxUtil getInstance() {
		 if(instance == null) { // 1
			 synchronized(XxxxUtil.class) { // 2
				 if(instance == null) { // 3
					 instance = new XxxxUtil(2,3); // 5
				 }
			 }
		 }
		 return instance; // 4
	}
}

这样以来, instance = new XxxxUtil(2,3); 创建对象的三个步骤就不会重排序了,原理是:被volatile修饰的变量,遵循一个happens-before原则 ” 对象的创建操作 happens-before 于该对象变量的赋值操作 “ 。关于指令重排序、happens-before 概念请自行百度学习理解

如果不使用以上DCL机制,还有另一种简单的,也是比较常的方法来实现单例的延迟初始化,那就是通过内部类的方式,代码如下:

public class XxxxUtil {
	private int a;
	private int b;
	private XxxxUtil(int a,int b) {
		this.a = a;
		this.b = b;
	}
	
	public static class XxxxUtilHolder{
		static XxxxUtil instance = new XxxxUtil(2,3);
	}
	
	public static XxxxUtil getInstance() {
		 return XxxxUtil.XxxxUtilHolder.instance;
	}
}

原理: 
1、由于内部类只有在使用时才会初始化,所以保证了单例的延迟初始化;
2、内部类的初始化过程,JVM内部实现已经避免了多线并发问题;