设计模式 单例模式
在开发中经常会用到单例设计模式,目的就是为了在程序的整个生命周期内,只会创建一个类的实例对象,而且只要程序不被杀死,该实例对象就不会被释放。 指在 JVM 范围内。
定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点
优缺点
1、由于单例模式要求在全局内只有一个实例,因而可以节省比较多的内存空间;
2、全局只有一个接入点,可以更好地进行数据同步控制,避免多重占用;
3、单例可长驻内存,减少系统开销。
单例模式的应用举例:
1、生成全局惟一的序列号;
2、访问全局复用的惟一资源,如磁盘、总线等;
3、单个对象占用的资源过多,如数据库等;
4、系统全局统一管理,如Windows下的Task Manager;
5、网站计数器。
缺点:
1、单例模式的扩展是比较困难的;
2、赋于了单例以太多的职责,某种程度上违反单一职责原则(六大原则后面会讲到);
3、单例模式是并发协作软件模块中需要最先完成的,因而其不利于测试;
4、单例模式在某种情况下会导致“资源瓶颈”。
实现方式:
在单例的类中设置一个 private 静态变量instance,instance 类型为当前类,用来持有单例唯一的实例。
将(无参数)构造器设置为 private,避免外部使用 new 构造多个实例。
提供一个 public 的静态方法,如 getInstance,用来返回该类的唯一实例 instance。
- 懒汉式 lazy initialization
- 饿汉式 eager initialization
- 双重校验锁 Double CheckLock DCL
- 枚举
- 静态内部类
懒汉式
懒汉模式是声明一个静态对象,并且在用户第一次调用getInstance时进行初始化,而上述的饿汉模式是在声明静态对象时就已经初始化,
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
synchronized 保证 线程安全。
延迟初始化
饿汉式
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
不足:
如果构造方法中存在过多的处理,会导致加载这个类时比较慢,可能引起性能问题。
如果使用饿汉式的话,只进行了类的装载,并没有实质的调用,会造成资源的浪费。
Double CheckLock DCL
public class SingleDCL {
private volatile static SingleDCL singleDCL = null;
private SingleDCL(){}
public static SingleDCL getSingleDCL(){
if(singleDCL == null){
synchronized (SingleDCL.class){
if(singleDCL == null){
singleDCL = new SingleDCL();
}
}
}
return singleDCL;
}
}
假设线程A执行到了instance=new Singleton2 ()语句,这里看起来是一句代码,但实际上它并不是一个原子操作,什么是原子操作?可以看这儿(http://baike.baidu.com/item/原子操作?fr=aladdin),这句代码最终会被编译成多条汇编指令,大致做了三件事:
(1)、给Singleton的实例分配内存;
(2)、调用Singleton()的构造函数,初始化成员字段;
(3)、将instance对象指向分配的内存空间(此时instance就不是null);
DCL的优点:资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。
DCL模式是使用最多的单例实现方式,它能够在需要时才实例化单例对象,并且能够在绝大多数场景下保证单例对象的唯一性,除非你的代码在并发场景比较复杂或者低于jdk1.6版本下使用,否则这种方式一般能够满足需求。
《Java并发编程实战》 p286 16.2.4 这种方式已被废弃。
静态内部类单例
DCL 有可能失效
枚举单例
写法简单是枚举单例最大的优点,枚举在Java中与普通的类是一样的,不仅能够有字段,还能够有自己的方法。最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。为什么?在上述的几种单例中,在一个情况下它们会出现重新创建对象的情况,那就是反序列化。
通过反序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效获得一个实例。即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的、被实例化的方法readResolve,这个方法可以让开发人员控制对象的反序列化。例如:上述几个实例中如果要杜绝单例对象在被饭序列化重新生成对象,那么必须加入如下方法:
/**
* 枚举单例
* Created by Brady on 2017/5/23.
*/
public enum SingleEnum {
INSTANCE;
}
容器实现单例
在程序的初始化时,将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时也可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。
不管以哪种形式实现单例模式,他们的核心原理都是讲构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取的过程中必须保证线程安全、防止反序列化导致重新生成实例对象等问题。选择哪种实现方式取决于项目本身,如是否是复杂的并发环境、jdk版本是否过低、单例对象的资源消耗等。
参考资料
设计模式系列 14-- 单例模式
Java设计模式(十) 你真的用对单例模式了吗?
Android设计模式——单例模式之源码使用场景(一)
单例模式的七种写法
1、Python与设计模式--单例模式