0.前言
写在最前面,本人的设计模式类博文,建议先看博文前半部分的理论介绍,再看后半部分的实例分析,最后再返回来复习一遍理论介绍,这时候你就会发现我在重点处标红的用心,对于帮助你理解设计模式有奇效哦~
1.单例模式介绍
单例模式定义:
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式的使用场景/优点:
(1)避免产生多个对象消耗过多的资源(特别是一个对象需要频繁的创建和销毁时);
(2)提供一个全局访问点,常常被用来管理系统中共享的资源(作为一个Manager)。
单例模式的缺点:
(1)单例模式一般没有接口,拓展困难;
(2)单例模式若持有Context,容易引起内存泄漏,最好传给单例对象Application Context。
单例模式的特点:
(1)确保单例类的对象有且只有一个,尤其在多线程环境下。并通过静态方法或枚举返回单例对象。
(2)构造函数不对外开放,一般为private。
(3)确保单例类对象在反序列化时不生成新对象。
2.单例模式实现
下面将会介绍单例模式不同的实现方式,各有优缺点,汇总如下。
2.1 饿汉单例模式
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() { } public static Single getInstance() { return instance; } }
从代码中可以看出饿汉单例模式的特点:
(1)不能通过new的形式构造对象(构造函数已经私有化);
(2)只能通过Singleton.getInstance()静态方法获取这个静态对象;
(3)加载此类时就创建了唯一实例,JVM保证在任何线程访问instance之前都会创建完毕;
(4)适合单例对象初始化占用内存小、快速并且使用频繁的情况。
2.2 懒汉单例模式(不建议使用)
public class Singleton { private static Singleton instance = null; private Singleton() { } public static synchronized Singleton getInstance() { if(null == instance) { instance = new Singleton(); } return instance; } }
从代码中可以看出懒汉单例模式的特点:
(1)添加了synchronized关键字,保证在多线程下单例对象唯一;不使用该关键字的懒汉模式不是线程安全的。
(2)单例只有在使用时才被实例化,一定程度上节约了资源,提高了效率;
(3)即使instance已经被实例化,依旧存在不必要的synchronized同步开销,效率大大降低因此不建议使用该方式。
(4)最后一个缺点就是第一次加载反应较慢。
2.3 双重校验锁(Double CheckLock)单例模式(使用最多!建议在JDK6及以上使用)
public class Singleton { private static Singleton instance = null; private Singleton(){ } public static Singleton getInstance() { if(null == instance) { synchronized (Singleton.class) { if(null == instance) instance = new Singleton(); } } return instance; } }
从代码中可以看出双重校验锁(Double CheckLock)单例模式的特点:
(1)明显继承了懒汉单例模式的所有优点和部分缺点(上述1、2、4条);
(2)第一次判空确保在被实例化后调用instance不再进行同步锁(明显是懒汉单例模式的升级版)。
(3)第二次判空的原因比较复杂,分析如下,关键是这行代码:
instance = new Singleton(); //此行代码会被编译成多条汇编指令,内部完成了三件事情 //第一,为Singleton实例分配内存; //第二,调用构造函数初始化成员字段; //第三,将instance对象指向分配的内存空间,此时instance!=null;
由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,在3执行完毕并且2未执行之前,被切换到其他线程上,这时候instance已经是非空了,其他线程直接拿走instance使用,然后顺理成章地就会报错。
JDK1.5之后(使用volatile关键字)只需要将instance的定义改成如下形式,就可以保证instance每次都从主内存读取,就可以使用DCL的写法来完成单例模式。
private volatile static Singleton instance = null;
volatile关键字作用:
1.这个变量不会在多个线程中存在复本,直接从内存读取。
2.这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会在被重排序到内存屏障之前。
2.4 静态内部类单例模式(推荐使用)
public class Singleton { private Singleton(){ } public static Singleton getInstance() { return SingleHolder.instance; } private static class SingleHolder { private static final Singleton instance = new Singleton(); } }
从代码中可以看出静态内部类单例模式的特点:
(1)第一次加载Singleton类并不会初始化instance,只有在第一次使用getInstance时才会导致初始化;
(2)第一次调用getInstance才会加载SingleHolder类,不仅能够确保线程安全,也能保证唯一性。
2.5 枚举单例模式(抗反序列化)
public enum Singleton{ INSTANCE; //可以有自己的方法 }
在使用时:
Singleton singleton = Singleton.INSTANCE;
从代码中可以看出枚举单例模式的特点:
(1)默认枚举实例的创建是线程安全的,并且在任何情况下都是一个单例;
(2)枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性不高,不建议用;
(3)不存在反序列化生成新实例的问题。分析如下:
通过反序列化可将一个单例的实例对象写到磁盘,然后再读出来,从而获得一个实例。即使构造函数是私有的,反序列化依旧可以通过特殊途径创建该类的一个新实例。反序列化操作提供了readResolve方法,这个方法可以让开发人员控制对象的反序列化。在上述的几个方法示例中如果要杜绝单例对象被反序列化是重新生成对象,就必须加入如下方法:
private Object readResolve() throws ObjectStreamException{ return singleton; //返回对象,而不是默认生成新对象 }
2.6 使用容器实现单例模式
public class SingletonManager { private static Map<String, Object> objMap = new HashMap<String,Object>(); private Singleton() { } public static void registerService(String key, Objectinstance) { if (!objMap.containsKey(key) ) { objMap.put(key, instance) ; } } public static ObjectgetService(String key) { return objMap.get(key) ; } }
从代码中可以看出这种单例模式的特点:
用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。