共计 5384 个字符,预计需要花费 14 分钟才能阅读完成。
在 GoF 的 23 种设计模式中,单例模式是比较简单的一种。然而,有时候越是简略的货色越容易呈现问题。上面就单例设计模式具体的探讨一下。
所谓单例模式,简略来说,就是在整个利用中保障只有一个类的实例存在。就像是 Java Web 中的 application,也就是提供了一个全局变量,用途相当宽泛,比方保留全局数据,实现全局性的操作等。
1. 最简略的实现
首先,可能想到的最简略的实现是,把类的构造函数写成 private 的,从而保障别的类不能实例化此类,而后在类中提供一个动态的实例并可能返回给使用者。这样,使用者就能够通过这个援用应用到这个类的实例了。
public class SingletonClass {
private static final SingletonClass instance = new SingletonClass();
public static SingletonClass getInstance() {
return instance;
}
private SingletonClass() {
}
}
如上例,内部使用者如果须要应用 SingletonClass 的实例,只能通过 getInstance()办法,并且它的构造方法是 private 的,这样就保障了只能有一个对象存在。
2. 性能优化——lazy loaded
下面的代码尽管简略,然而有一个问题——无论这个类是否被应用,都会创立一个 instance 对象。如果这个创立过程很耗时,比方须要连贯 10000 次数据库(夸大了…:-)),并且这个类还并不一定会被应用,那么这个创立过程就是无用的。怎么办呢?
为了解决这个问题,咱们想到了新的解决方案:
public class SingletonClass {
private static SingletonClass instance = null;
public static SingletonClass getInstance() {
if(instance == null) {
instance = new SingletonClass();
}
return instance;
}
private SingletonClass() {
}
}
代码的变动有两处——首先,把 instance 初始化为 null,直到第一次应用的时候通过判断是否为 null 来创建对象。因为创立过程不在申明处,所以那个 final 的润饰必须去掉。
咱们来设想一下这个过程。要应用 SingletonClass,调用 getInstance()办法。第一次的时候发现 instance 是 null,而后就新建一个对象,返回进来;第二次再应用的时候,因为这个 instance 是 static 的,所以曾经不是 null 了,因而不会再创建对象,间接将其返回。
这个过程就成为 lazy loaded,也就是迟加载——直到应用的时候才进行加载。
3. 同步
下面的代码很分明,也很简略。然而就像那句名言:“80% 的谬误都是由 20% 代码优化引起的”。单线程下,这段代码没有什么问题,可是如果是多线程,麻烦就来了。咱们来剖析一下:
线程 A 心愿应用 SingletonClass,调用 getInstance()办法。因为是第一次调用,A 就发现 instance 是 null 的,于是它开始创立实例,就在这个时候,CPU 产生工夫片切换,线程 B 开始执行,它要应用 SingletonClass,调用 getInstance()办法,同样检测到 instance 是 null——留神,这是在 A 检测完之后切换的,也就是说 A 并没有来得及创建对象——因而 B 开始创立。B 创立实现后,切换到 A 继续执行,因为它曾经检测完了,所以 A 不会再检测一遍,它会间接创建对象。这样,线程 A 和 B 各自领有一个 SingletonClass 的对象——单例失败!
解决的办法也很简略,那就是加锁:
public class SingletonClass {
private static SingletonClass instance = null;
public synchronized static SingletonClass getInstance() {
if(instance == null) {
instance = new SingletonClass();
}
return instance;
}
private SingletonClass() {
}
}
是要 getInstance()加上同步锁,一个线程必须期待另外一个线程创立实现后能力应用这个办法,这就保障了单例的唯一性。
4. 又是性能
下面的代码又是很分明很简略的,然而,简略的货色往往不够现实。这段代码毫无疑问存在性能的问题——synchronized 润饰的同步块可是要比个别的代码段慢上几倍的!如果存在很屡次 getInstance()的调用,那性能问题就不得不思考了!
让咱们来剖析一下,到底是整个办法都必须加锁,还是仅仅其中某一句加锁就足够了?咱们为什么要加锁呢?剖析一下呈现 lazy loaded 的那种情景的起因。起因就是检测 null 的操作和创建对象的操作拆散了。如果这两个操作可能原子地进行,那么单例就曾经保障了。于是,咱们开始批改代码:
public class SingletonClass {
private static SingletonClass instance = null;
public static SingletonClass getInstance() {
synchronized (SingletonClass.class) {
if(instance == null) {
instance = new SingletonClass();
}
}
return instance;
}
private SingletonClass() {
}
}
首先去掉 getInstance()的同步操作,而后把同步锁加载 if 语句上。然而这样的批改起不到任何作用:因为每次调用 getInstance()的时候必然要同步,性能问题还是存在。如果……如果咱们当时判断一下是不是为 null 再去同步呢?
public class SingletonClass {
private static SingletonClass instance = null;
public static SingletonClass getInstance() {
if (instance == null) {
synchronized (SingletonClass.class) {
if (instance == null) {
instance = new SingletonClass();
}
}
}
return instance;
}
private SingletonClass() {
}
}
还有问题吗?首先判断 instance 是不是为 null,如果为 null,加锁初始化;如果不为 null,间接返回 instance。
这就是 double-checked locking 设计实现单例模式。到此为止,所有都很完满。咱们用一种很聪慧的形式实现了单例模式。
5. 从源头查看
上面咱们开始说编译原理。所谓编译,就是把源代码“翻译”成指标代码——大多数是指机器代码——的过程。针对 Java,它的指标代码不是本地机器代码,而是虚拟机代码。编译原理外面有一个很重要的内容是编译器优化。所谓编译器优化是指,在不扭转原来语义的状况下,通过调整语句程序,来让程序运行的更快。这个过程成为 reorder。
要晓得,JVM 只是一个规范,并不是实现。JVM 中并没有规定无关编译器优化的内容,也就是说,JVM 实现能够自在的进行编译器优化。
上面来想一下,创立一个变量须要哪些步骤呢?一个是申请一块内存,调用构造方法进行初始化操作,另一个是调配一个指针指向这块内存。这两个操作谁在前谁在后呢?JVM 标准并没有规定。那么就存在这么一种状况,JVM 是先开拓出一块内存,而后把指针指向这块内存,最初调用构造方法进行初始化。
上面咱们来思考这么一种状况:线程 A 开始创立 SingletonClass 的实例,此时线程 B 调用了 getInstance()办法,首先判断 instance 是否为 null。依照咱们下面所说的内存模型,A 曾经把 instance 指向了那块内存,只是还没有调用构造方法,因而 B 检测到 instance 不为 null,于是间接把 instance 返回了——问题呈现了,只管 instance 不为 null,但它并没有结构实现,就像一套房子曾经给了你钥匙,但你并不能住进去,因为外面还没有拾掇。此时,如果 B 在 A 将 instance 结构实现之前就是用了这个实例,程序就会呈现谬误了!
于是,咱们想到了上面的代码:
public class SingletonClass {
private static SingletonClass instance = null;
public static SingletonClass getInstance() {
if (instance == null) {
SingletonClass sc;
synchronized (SingletonClass.class) {
sc = instance;
if (sc == null) {
synchronized (SingletonClass.class) {
if(sc == null) {
sc = new SingletonClass();
}
}
instance = sc;
}
}
}
return instance;
}
private SingletonClass() {
}
}
咱们在第一个同步块外面创立一个长期变量,而后应用这个长期变量进行对象的创立,并且在最初把 instance 指针长期变量的内存空间。写出这种代码基于以下思维,即 synchronized 会起到一个代码屏蔽的作用,同步块外面的代码和内部的代码没有分割。因而,在内部的网站交易同步块外面对长期变量 sc 进行操作并不影响 instance,所以外部类在 instance=sc; 之前检测 instance 的时候,后果 instance 仍然是 null。
不过,这种想法齐全是 谬误 的!同步块的开释保障在此之前——也就是同步块外面——的操作必须实现,然而并不保障同步块之后的操作不能因编译器优化而调换到同步块完结之前进行。因而,编译器齐全能够把 instance=sc; 这句移到外部同步块外面执行。这样,程序又是谬误的了!
6. 解决方案
说了这么多,难道单例没有方法在 Java 中实现吗?其实不然!
在 JDK 5 之后,Java 应用了新的内存模型。volatile 关键字有了明确的语义——在 JDK1.5 之前,volatile 是个关键字,然而并没有明确的规定其用处——被 volatile 润饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因而,只有咱们简略的把 instance 加上 volatile 关键字就能够了。
public class SingletonClass {
private volatile static SingletonClass instance = null;
public static SingletonClass getInstance() {
if (instance == null) {
synchronized (SingletonClass.class) {
if(instance == null) {
instance = new SingletonClass();
}
}
}
return instance;
}
private SingletonClass() {
}
}
然而,这只是 JDK1.5 之后的 Java 的解决方案,那之前版本呢?其实,还有另外的一种解决方案,并不会受到 Java 版本的影响:
public class SingletonClass {
private static class SingletonClassInstance {
private static final SingletonClass instance = new SingletonClass();
}
public static SingletonClass getInstance() {
return SingletonClassInstance.instance;
}
private SingletonClass() {
}
}
在这一版本的单例模式实现代码中,咱们应用了 Java 的动态外部类。这一技术是被 JVM 明确阐明了的,因而不存在任何二义性。在这段代码中,因为 SingletonClass 没有 static 的属性,因而并不会被初始化。直到调用 getInstance()的时候,会首先加载 SingletonClassInstance 类,这个类有一个 static 的 SingletonClass 实例,因而须要调用 SingletonClass 的构造方法,而后 getInstance()将把这个外部类的 instance 返回给使用者。因为这个 instance 是 static 的,因而并不会结构屡次。
因为 SingletonClassInstance 是公有动态外部类,所以不会被其余类晓得,同样,static 语义也要求不会有多个实例存在。并且,JSL 标准定义,类的结构必须是原子性的,非并发的,因而不须要加同步块。同样,因为这个结构是并发的,所以 getInstance()也并不需要加同步。
至此,咱们残缺的理解了单例模式在 Java 语言中的时候,提出了两种解决方案。集体偏差于第二种,并且 Effiective Java 也举荐的这种形式。