【那座山,正当顶上,有一块仙石。其石有三丈六尺五寸高,有二丈四尺围圆。三丈六尺五寸高,按周天三百六十五度;二丈四尺围圆,按政历二十四气。上有九窍八孔,按九宫八卦。四面更无树木遮阴,左右倒有芝兰相衬。盖自开辟以来,每受天真地秀,日精月华,感之既久,遂有灵通之意。内育仙胞,一日迸裂,产一石卵,似圆球样大。因见风,化作一个石猴,五官俱备,四肢皆全。便就学爬学走,拜了四方。目运两道金光,射冲斗府。】
上面这段文字,描述了悟空出生时的场景。孙悟空只有一个,任何程序要使用孙悟空这个对象,都只能使用同一个实例。
所以,单例模式非常好理解,单例模式确保一个类只有一个实例,且这个类自己创建自己的唯一实例并向整个系统提供这个实例,这个类叫做单例类。
其实,这个设计模式与抽象思维或者业务架构设计没有太多关系,更多要求的是对Java内存模型以及并发编程的理解,所以在介绍单例模式之前,需要先介绍一下JMM(Java Memory Model)相关的基础知识,然后再理解单例模式就会简单得多。
1.重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序又包括编译器优化的重排序、指令级并行的重排序以及内存系统的重排序。
比如下面一段代码:
int a = 1;//A
int b = 2;//B
A并不一定是比B先执行的,它们的执行顺序可能是A-B,也可能是B-A,甚至有可能是一同执行的。
2.happens-before与as-if-serial
as-if-serial保证单线程内程序的执行结果不被改变,它给程序员一个幻觉:单线程程序是按程序的顺序来执行的;
happens-before保证正确同步的多线程程序的执行结果不被改变,它给程序员一个幻觉:正确同步的多线程程序是按happens-before指定的顺序来执行的。
程序员其实并不关心两个指令是否真的被重排序了,我们只关心程序执行的语义不能被改变,也就是程序的执行结果不能改变。
比如上面那段代码的A与B顺序颠倒过来,对程序的结果并没有影响,我们还是可以获得两个赋值正确的int变量。但如果是下面这段代码,就有问题了:
int x = 1;//A
int x = 2;//B
如果这两行代码的执行顺序发生了改变,那么我们最终得到的x的值可能不是2,而是1,那样程序的执行结果就发生了改变了。好在JMM对于这种有数据依赖性(两个指令都是对同一个变量进行的)的重排序已经禁止了,所以我们并不需要担心。
3.类初始化锁
Java语言规范规定,对于每一个类或者接口A,都有一个唯一的初始化锁LA与之对应。从A到LA的映射,由JVM的具体实现去自由实现。JVM在类初始化期间会获取这个初始化锁,并且每个线程至少获取一次锁来确保这个类已经被初始化过了。这个锁可以同步多个线程对同一个类的初始化。
4.volatile的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存;当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
5.JSR-133内存模型
从JDK5开始,java升级了内存模型,开始使用JSR-133内存模型。JSR-133对旧内存模型的修补主要有两个:增强volatile的内存语义,严格限制volatile变量与普通变量的重排序(旧内存模型允许volatile变量与普通变量重排序);增强final的内存语义,使final具有初始化安全性,在旧的内存模型中,多次读取同一个final变量的值可能会不同。
下面我们再来开始看单例模式的各种实现方式,也许你还对上面这些概念不是很熟悉,但结合具体的代码,相信会加深你的理解。
饿汉模式
package com.tirion.design.singleton;
public class WuKong {
private static WuKong wuKong = new WuKong();
private WuKong() {
}
public static WuKong getWuKong() {
return wuKong;
}
}
static变量会在类装载的时候完成初始化,这里注意构造方法也被声明为private,我们只能通过WuKong.getWuKong()来获取WuKong的唯一实例wuKong静态变量。
因为单例的实现是在类装载的时候完成的,并且无论后面对象实例是否被真正用到(WuKong.getWuKong()会不会得到执行),对象实例都已经被创建了,所以把这种以空间换时间的方式成为饿汉模式。
饿汉模式的优缺点也非常明显,它不必等到用到的时候再创建实例,节省了程序的运行时间,但在某些情况下也可能创建了不必要的对象,导致空间被浪费。
懒汉模式
package com.tirion.design.singleton;
public class WuKong {
private static WuKong wuKong = null;
private WuKong() {
}
public static synchronized WuKong getWuKong() {
if (wuKong == null) {
wuKong = new WuKong();
}
return wuKong;
}
}
懒汉模式与饿汉模式的不同之处在于把实例对象的创建放到了静态工厂方法内部,当调用WuKong.getWuKong()时,会判断实例是否已经被创建,如果没有创建则进行实例对象的初始化工作,已经创建则直接返回。
懒汉模式为了实现多线程环境下的线程安全,在创建实例的方法上增加了synchronized同步控制,顺便说一下synchronized是编译器通过插入monitorenter和monitorexit指令来进行同步控制的,所有调用synchronized方法的线程都要在monitorenter处等待获取monitor对象锁,所以导致懒汉模式在线程竞争环境下效率非常低,这也是称之为懒汉模式的原因。
基于volatile的DCL双重检查锁机制的单例
1 package com.tirion.design.singleton;
2
3 public class WuKong {
4 private static volatile WuKong wuKong = null;
5
6 private WuKong() {
7 }
8
9 public static WuKong getWuKong() {
10 if (wuKong == null) {
11 synchronized (WuKong.class) {
12 if (wuKong == null) {
13 wuKong = new WuKong();
14 }
15 }
16 }
17 return wuKong;
18 }
19 }
我们发现,双重锁检查机制相比于懒汉模式,又有几个细节被改动:
a.静态工厂方法的synchronized被去掉了,改为使用同步代码块来进行控制
b.从原先的一次判断对象实例是否为null改为了两次判断
c.对象实例增加了volatile关键词修饰
下面我们来对这几个细节一一进行分析,看看这些改动有哪些意义:
针对第一个改动,我们从懒汉模式的分析中已经可以看出,synch