Java设计模式:Singleton(单例)模式
概念定义
Singleton(单例)模式是指在程序运行期间, 某些类只实例化一次,创建一个全局唯一对象。因此,单例类只能有一个实例,且必须自己创建自己的这个唯一实例,并对外提供访问该实例的方式。
单例模式主要是为了避免创建多个实例造成的资源浪费,以及多个实例多次调用容易导致结果出现不一致等问题。例如,一个系统只能有一个窗口管理器或文件系统,一个程序只需要一份全局配置信息。
应用场景
- 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如缓存、日志对象、应用配置。
- 控制资源的情况下,方便资源之间的互相通信。如数据库连接池、线程池等。
单例实现
根据加载的时机可以分为即时加载和延时加载两种模式。
即时加载
在单例类被加载时就创建单例的方式,称为即时加载单例(也称饿汉式)。
枚举类单例(推荐方式)
示例代码如下:
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance() { // 照顾开发者旧有习惯
return INSTANCE;
}
// 外部可调用EnumSingleton.INSTANCE.doSomething()或EnumSingleton.getInstance().doSomething()
public void doSomething() {
System.out.println("EnumSingleton: do something like accessing resources");
}
}
此类单例具有以下优点:
- 简洁高效
- 实例是静态的,线程安全
- 不存在clone、反射、序列化破坏单例问题
缺点则有:
- 枚举单例不能继承和被继承
- 可读性稍低(主要因为此方式较为"新颖")
静态公有域单例
示例代码如下:
public class StaticFieldSingleton {
public static final StaticFieldSingleton INSTANCE = new StaticFieldSingleton();
private StaticFieldSingleton() { // 私有化构造方法,防止外部实例化而破坏单例
if (INSTANCE != null) { // 防止反射攻击
throw new UnsupportedOperationException();
}
}
// 外部可调用StaticFieldSingleton.INSTANCE.doSomething()
public void doSomething() {
System.out.println("StaticFieldSingleton: do something like accessing resources");
}
}
静态工厂方法单例
示例代码如下:
public class StaticMethodSingleton {
private static final StaticMethodSingleton INSTANCE = new StaticMethodSingleton(); // INSTANCE由private修饰
private StaticMethodSingleton() {
if (INSTANCE != null) { // 防止反射攻击
throw new UnsupportedOperationException();
}
}
public static StaticMethodSingleton getInstance() {
return INSTANCE;
}
// 外部可调用StaticFieldSingleton.getInstance().doSomething()
public void doSomething() {
System.out.println("StaticMethodSingleton: do something like accessing resources");
}
}
静态工厂方法比静态公有域单例更具灵活性:
- 内部可以改变单例实现方式,例如将即时加载改造成延时加载/懒加载,保持API不变。
- 甚至可以改变类是否是单例。例如业务场景有所改变,将原先的单例变成非单例,也能保持API不变。
延时加载
即时加载相对简单,作为主要推荐的单例模式。但在有些业务场景中,不希望单例被过早创建,而在真正使用的那刻才创建,即延时加载单例(也称懒汉式)。此类场景有:
- 创建实例的开销很大,但访问频率却很低。
- 单例的创建依赖于其他资源的创建,为保证数据完整性必须延迟创建。
- ...
静态内部类单例(推荐方式)
示例代码如下:
public class StaticHolderSingleton {
private static class SingletonHolder {
private static final StaticHolderSingleton INSTANCE = new StaticHolderSingleton();
}
private StaticHolderSingleton() {}
public static StaticHolderSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
// 外部可调用StaticHolderSingleton.getInstance().doSomething()
public void doSomething() {
System.out.println("StaticHolderSingleton: do something like accessing resources");
}
}
静态内部类单例有以下特点:
- 只有当getInstance()方法被外部首次调用时,SingletonHolder类才被JVM加载和初始化,静态属性INSTANCE也跟着被初始化,从而达到延迟加载的目的。
- JVM保证初始化SingletonHolder类时,具有线程安全性,因此不会增加任何性能成本和空间浪费。
双重校验锁(DCL)单例
示例代码如下:
public class DCLSingleton {
private static volatile DCLSingleton instance; // volatile禁止指令重排序,并保证内存可见性
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) { // 此处判空旨在提高性能
synchronized (DCLSingleton.class) {
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
// 外部可调用DCLSingleton.getInstance().doSomething()
public void doSomething() {
System.out.println("DCLSingleton: do something like accessing resources");
}
}
DCL单例比较复杂,而且用到synchronized和volatile,性能有所损失。
破坏单例模式的方法
Java对象可通过new、克隆(clone)、反序列化(serialize)、反射(reflect)等方式创建。
通过私有化或不提供构造方法,可阻止外部通过new创建单例实例。其他几种创建方式则需要特别注意(枚举单例不存在本节风险)。
克隆
java.lang.Obeject#clone()
方法不会调用构造方法,而是直接从内存中拷贝内存区域。因此,单例类不能实现Cloneable接口。
反射
反射通过调用构造方法生成新的对象,可在构造方法中进行判断,实例已创建时抛出异常,如StaticFieldSingleton所示。
反序列化
普通Java类反序列化时会通过反射调用类的默认构造方法来初始化对象。如果单例类实现java.io.Serializable接口, 就可以通过反序列化破坏单例。
因此,单例类尽量不要实现序列化接口。如若必须,可以重写反序列化方法readResolve()
, 反序列化时直接返回相关单例对象:
public Object readResolve() {
return instance;
}
单例 vs 静态方法
单例:在一个JVM中只允许一个实例存在。单例常常是带有状态的,可以携带更丰富的信息,使用场景更加广泛。
- 单例是面向对象的
- 有状态的
- 方法跟实例是相关的
- 人为保证线程安全
- 能实现接口或者继承一个超类
静态方法: 对于不需要维护任何状态,仅提供全局访问方法的类,可将其实现为更简单的静态方法类(如各种Uitls工具类),它的速度更快。
- 静态方法是面向过程的
- 无状态的
- 方法跟实例是无关的
- 天然线程安全
- 静态方法速度更快(其绑定在编译期就进行)
业界实践
- java.lang.Runtime.getRuntime(JDK)
- java.util.concurrent.TimeUnit(JDK)
- 无数开源软件
要点总结
- 单例模式按加载时机可分为即时加载和延时加载两种方式。
- 即时加载有:枚举类单例、静态公有域单例和静态工厂方法单例。
- 推荐程度: 枚举类单例 > 静态工厂方法单例 > 静态公有域单例。
- 特例:若单例类必须要继承某个超类,则不宜使用枚举类单例。
- 延时加载有:静态内部类单例和双重校验锁(DCL)单例。
- 推荐静态内部类单例。
- 应避免使用双重校验锁单例。
- 若无特殊需要,优先使用即时加载模式的单例。
- 对于一些无状态的具有"唯一"特征的类(如工具类),建议使用静态方法实现。