定义
单例模式(Singleton Pattern)限制系统中某一个类只能有一个唯一的实例。很多时候系统对类的需求就只是一个全局对象,有些资源比较重,加载创建耗时,适用于单例模式;有些资源代表的是纯函数的操作,虽然可以使用new 来创建新对象,使用单例模式可以减少对象创建消耗,在手机等资源少的地方推荐使用。
使用场景
- 资源加载创建耗时,而资源是可共享的,比如日志文件,应用配置,打印机、显示器等
- 系统资源,如线程池
- 可复用的类资源,如纯函数的 Service,工具类
- 控制访问需求,如一个类要控制类实例的权限
- 全局的状态,比如统计访问人数,系统的状态转换等
单例模式的创建方式
通常的单例获取是类提供一个 getInstance
的静态方法,外部对象使用静态方法获取到类实例。
预加载
在类加载后直接实例化单例,系统控制并发,缺点是如果这个类从始至终都没有被使用过,加载实例浪费了系统资源。
public final class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
懒加载(延迟加载)
针对预加载可能浪费资源的问题,可以使用懒加载来避免。懒加载只有在资源被使用到的时候才会去加载。
public final class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
如果存在多线程并发的情况,上面的获取实例方法存在竞态条件,有两种解决方案。
synchronized
同步方法
public final class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
由于加了同步,因此多线程访问时只能串行访问,牺牲了效率保证了安全。
- double check lock
public final class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
双重检查保证了只有实例还未初始化时才会竞争锁,当实例初始化后,与第一种懒加载方式执行路径是一样的,并且减少了锁争抢开销。(jdk1.5 之后才可以使用)
懒加载虽然保证了资源加载就一定是被使用的,但是如果资源加载时间较长,所以使用方都必须等待资源加载完成,譬如数据库的加载、连接建立,因此要看自己的使用场景来判断是否需要使用懒加载模式。
静态内部类
静态内部类严格说起来属于懒加载,使用了jvm 加载类的机制,避免了使用双重检测的方式来防止并发问题。
public final class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
return Holder.holder;
}
private static class Holder {
private static Singleton holder = new Singleton();
}
}
在 Singleton
类加载时内部类并没有被加载,因此内部类的 holder
未被实例化,调用 getInstance
方法时,才真正加载和实例化。
枚举
枚举天生自带的单一属性非常符合单例模式,使用枚举的解决方案也非常优雅,而且还适合与策略、命令等模式配合。
枚举相比于前面的实现有几个优点:
- 无需费力将构造函数设为
private
,而且设为了private
也无法阻止使用方使用反射来创建实例。 - 无需担心序列化与反序列化,上面的模式中都没有考虑序列化相关问题,如果考虑还需要加入
readObject
writeObject
方法。 - 虽然模式叫单例模式,但有时我们可能需要 n 个实例,数量是需要控制的,增加了实现难度,枚举只需要加一个枚举值
- 可以与策略模式相配合,实现更优雅。
当然枚举也是一种预加载的实现方案,适合简单的单例,对于复杂的单例实现像数据库引擎等的还是前面的方式更合适一些。
枚举的实现
- 简单枚举
public enum Singleton {
/**
* 实例
*/
INSTANCE;
/**
*
*/
public void doSomething() {
}
}
- 与策略、命令等模式结合
如果我们每个实例需要有不同的实现,那么可以在枚举实例中覆盖相关方法, 枚举也支持抽象方法。
public enum SingletonEnum {
/**
* 实例
*/
INSTANCE1 {
@Override
public void forOverride() {
System.out.println("Instance1");
}
},
INSTANCE2 {
@Override
public void forOverride() {
System.out.println("Instance2");
}
};
/**
*
*/
public void doSomething() {
}
public abstract void forOverride();
}
总结
单例模式在后端自己创建的场景已经不多了,Spring 框架完全代理了相关操作,为开发者提供了很大的遍历,但是 Spring 本质上是一个工厂,有些复杂场景还是需要自己来动手。