介绍
单例模式可以说是很多开发者解除到的第一个设计模式,当然也可能用到了自己没发觉是设计模式,单例模式的核心思想莫过于创建类的唯一实例,从而避免类的重复创建,达到节约资源开销的目的,从而满足业务上的实际需求。
定义
确保某个类有且只有一个实例,且只有自己可实例化
饿汉模式
这是最浅显的写法,直接上代码
public class Person {
public static Person mInstant = new Person();
public static Person getInstance(){
return mInstant;
}
private Person() {
}
}
这是最普通的单例模式,缺点也很明显就是类的实例在一开始就创建了,难免浪费资源,理想状况当然是使用的时候再创建,于是便有了懒汉模式
懒汉模式
话不多少,先上代码
public class Person {
private static Person mInstant = null;
public static Person getInstance(){
if (mInstant == null) {//1
mInstant = new Person();//2
}
return mInstant;
}
private Person() {
}
}
可以看到,就是在实际用到的时候才去实例化,这样确实可以省内存开销,并且在单线程运行下是没有问题的,多线程就不一定了。假设有A、B两个线程同时调用getInstance(),此时都走到第1步,mInstant还没有初始化,所以AB两个线程会拿到两个Person对象,这就与我们单例模式的定义不符。大家可能会认为这种概率比较小不用考虑,实际中并发编程是我们用的最多的,小概率也是代表会发生,在不考虑机器出错的情况下,程序员的使命就是应该让程序尽可能无bug。所以这时候关键字synchronized
public class Person {
private static Person mInstant = null;
public synchronized static Person getInstance(){
if (mInstant == null) {
mInstant = new Person();
}
return mInstant;
}
private Person() {
}
}
或是
public class Person {
private static Person mInstant = null;
public static Person getInstance(){
synchronized (Person.class) {
if (mInstant == null) {
mInstant = new Person();
}
}
return mInstant;
}
private Person() {
}
}
实际上这两种方式大同小异,是能够达到多线程只有一个实例的目的,但缺点也很明显,就是效率太低了,试想一下,每次调用方法都需要synchronized 一次,那对性能会有很大的影响,所以就有了DCL(double checked locking)单例模式
DCL单例
public class Person {
private static Person mInstant = null;
public static Person getInstance(){
if (mInstant == null) {//1
synchronized (Person.class) {//2
if (mInstant == null) {//3
mInstant = new Person();
}
}
}
return mInstant;
}
private Person() {
}
}
可以看到DCL模式使用了双重校验,即使在多线程中线程AB都已经到2,此时A得到线程锁开始实例化,之后释放锁,线程B再进来也会判空校验,从而避免了创建多个对象的情况。
那么这样就万无一失了嘛?答案是否定的,因为编译器会对指令进行优化排序(优化排序指的是编译器在不改变单线程语义的情况下,可以重新安排程序的执行顺序)。
我们来看下new一个对象的时候,优化排序前会进行如下操作:
1.分配一块内存M
2.在内存M上实例化Person
3.将内存M地址赋予mInstant
经过优化排序后,编程如下:
1.分配一块内存M
2.将内存M地址赋予mInstant
3.在内存M上实例化Person
在单线程中,这样确实没有改变语义并且运行结果也是预期中,但在多线程中会有问题,上图:
在图中,如若A在内存M赋值时,线程B进行了非空判断,要知道mInstant == null比较的就是内存地址,而null内存地址默认为000000,而B拿到了M的内存比较,直接返回了mInstant,此时B使用的对象进行操作直接报空指针异常。
那么,有没有办法能够解决呢?
答案是肯定过的,在JDK1.5对关键字 volatile 进行优化,保证volatile修饰的变量不会被编译器进行优化排序
public class Person {
private volatile static Person mInstant = null;
public static Person getInstance(){
if (mInstant == null) {
synchronized (Person.class) {
if (mInstant == null) {
mInstant = new Person();
}
}
}
return mInstant;
}
private Person() {
}
}
这样就保证了消耗资源少的同时不会出错,当然你可以用静态内部类单例。
静态内部类单例
public class Person {
public static Person getInstance(){
return PersonHolder.PERSON_HOLDER;
}
private static class PersonHolder{
private static Person PERSON_HOLDER = new Person();
}
private Person() {
}
}
这样写既可以保证不使用时不预创建浪费不必要的资源,又能保证在多线程时调用获取到同一个实例,那么实例化的时机怎么回事呢?我们来了解一下类的加载时机:
1.对类的new操作、静态变量的读写、静态方法的调用。
2.对子类进行初始化时,父类未初始化会进行初始化。
3.虚拟机启动时需要指定一个包含main方法的主类会对其进行初始化
4.对类进行的反射。
5.JDK1.7之后,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
.所以对类的静态内部属性进行引用,这时才会去初始化类的静态变量。那么此时多线程调用如何保证不会进行多次实例化呢?首先,在一个类加载器中,类只会初始化一次。其次,多线程初始化同一个类时,除了在进行初始化的线程,其余的都会阻塞等待,直到初始化完成。
枚举型单例
public enum PersonEnum {
INSTANT;
}
就这么简单,枚举也算单例的一种,默认在初始化时进行实例化,同样的消耗资源,但枚举在源码中实现了线程安全,防反射和反序列化,感兴趣的自行百度,本文不再深究。
总结
本文介绍了五种单例模式,总的来说各有优缺点,不考虑资源浪费使用饿汉和枚举,考虑线程安全选择静态内部类单例,考虑安全选择枚举。