单例模式
单例模式(Singleton Pattern)是一个比较简单的模式,其定义如下:
Ensure a class has only one instance, and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)
单例模式通用类图

Singleton 类成为单例类,通过使用 private 修饰构造函数,确保出自己以外别的地方都不是能使用构造函数创建实例;通过自身确保在一个应用中只产生 一个实例,并且是自行实例化的。如下代码所示:
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
/**
* 确保除自身以外,都不允许使用 new 创建实例
*/
private Singleton() {
}
/**
* 通过该方法获取实例对象
*/
public static Singleton getInstance() {
return INSTANCE;
}
}
单例模式的优点
由于单例模式在内存中只有一个实例,减少了内存开支,特别是在一个对象需要频繁创建、销毁时,创建和销毁时的性能是难以优化的;
由于单例模式只创建一个实例,所以减少了系统的性能开销,当一个对象的创建需要比较多的资源,如读取配置、产生其它依赖对象时, 则可以通过在应用启动时直接创建一个单例对象,然后用永久驻留内存的方式来解决
(在 JavaEE 中采用单例模式时需要注意 JVM 垃圾回收机制)
;单例模式可以避免对系统资源的多重占用,例如一个写文件动作,所有需要写文件都使用一个单例对象,可避免对同一资源文件的同时写操作(涉及线程安全问题,需要结合 synchronized 关键字);
单例模式可以在系统设置全局访问点,优化和共享资源的访问,例如可以设计一个单例类,负责所有数据表的映射处理。
单例模式的缺点
单例模式一般没有接口,扩展很困难,如需要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢? 因为接口对单例模式是没有任何意义的,它需要“自行实例化”,并且提供单一实例,很显然接口或抽象类是不可能被实例化的。 当然,在特殊情况,单例类可以实现接口、被继承等,需要在系统开发中根据实际情况进行判断;
单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用 mock 的方式虚拟一个对象;
单例模式与
单一原则
有冲突。一个类应该只实现一个逻辑,而不关心它是否单例的,是不是要单例取决于环境,单例模式把“要单例”与业务逻辑融合在一个类中。
单例模式的使用场景
在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反映”,可以采用单例模式,具体的场景如下:
要求创建唯一序列号的环境;
在整个项目中需要一个共享点或共享数据,例如一个 Web 页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的。
创建一个对象需要消耗的资源过多,如要访问 IO 和数据库等资源;
需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为 static 的方式)。
单例模式的注意事项
在高并发的情况下,需要注意单例模式的线程同步问题。单例模式有几种不同的创建方式,之前的例子不会出现产生多个实例的情况,但是接下来演示的单例模式就需要考虑线程安全:
public class Singleton {
private static Singleton instance;
/**
* 确保除自身以外,都不允许使用 new 创建实例
*/
private Singleton() {
}
/**
* 通过该方法获取实例对象
*/
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
该单例模式也被成为饿汉式单例
(前面第一个例子也被称为懒汉式单例
)。饿汉式单例存在现在安全问题,也就是系统中可能会出现两个单例类的实例对象, 设想一下,当系统中有两个线程 A 和 B 同时去调用 getInstance()
(此时 instance 还为空),线程 A 与线程 B 在执行 instance == null
可能都会得到 true, 那么就会执行 instance = new Singleton()
,直接导致内存中存在两个单例对象。
解决线程线程安全的方法有很多,例如使用 synchronized
修饰 getInstance() 方法,也可以使用 synchronized 在 getInstance() 方法中创建一个同步块。 但就该单例模式而言,最好是使用懒汉式单例,这比使用 synchronized 来防止创建多个单例类对象要好得多。
除了线程安全问题会导致出现两个单例对象,还有一种方式也可以 -- 对象复制
。在 Java 中对象复制默认是不允许的,需要实现 Cloneable 接口,并重写 clone() 方法, 方可直接通过对象赋值方法获取一个新的对象;对象的赋值是不通过构造函数的,所有这里私有化构造函数是不能阻止对象赋值的。通常情况下,单例类赋值的情况是不需要担心的, 除非你主动实现了 Cloneable 接口。
单例模式的扩展
多例模式,它是单例模式的一种扩展,它可以根据系统需要创建固定数量对象。采用有上限的多例模式,可以在程序应用设计时决定内存中有多少个实例,方便系统进行扩展,修正单例模式可能存在的性能问题,提供系统响应速度。例如读取文件,我们可以在系统启动时完成初始化工作,在内存中启动固定数量的 reader 实例,这样在需要读取文件时就可以快速响应。
总结
单例模式是 23 种设计模式种比较简单的模式,引用也非常广泛,如在 Spring 中,每个 Bean 默认就是单例的,这样做的优点是 Spring 容器可以管理这些 Bean 的生命周期,决定什么时候创建,什么时候销毁,销毁的时候要如何处理等等。如果在 Spring 中采用非单例模式(Prototype 类型),则 Bean 初始化后的管理 交由 J2EE 容器,Spring 容器不再跟踪管理 Bean 的生命周期。
摘自:《设计模式之禅》(第 2 版)