唯一实例与中心化——单例模式
1. 定义
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
一句话总结,就是唯一实例,中心化
现实世界的模型:
- 日本只有一个天皇。
- 一个国家只有一个央行。
单例模式需要满足以下的特点:
- 单例类全局只有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一个实例。
2. 设计
单例类的类图可以简单表示为: 单例模式如果正确地设计单列,需要关注两个点:
- 线程安全。
- 性能。
因为单例类需要自己实例化自身,并且要确保在多线程环境下不会产生多个实例,而且并发下性能到达最优。
根据实例化的时机,分为两种:
-
饿汉模式,类加载的时候就初始化。
-
懒汉模式,延迟初始化。
2.1. 懒汉:错误方式
为了实现懒汉,延迟初始化单例,我们把实例化的过程推迟到第一次访问单例上。于是就有了这样的代码
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
看上去很美好,实则有一个很大的隐患,多线程调用 getInstance
方法会产生多实例。
简单分析一下,假设 A 线程和 B 线程同时调用
- A 线程执行
instance = new Singleton();
,语句还没结束, instance 还为 null - 这时候 B 线程进入语句
if (instance == null)
,条件成立,也进入instance = new Singleton();
- 两个线程均建立了实例
为了确保能够延迟初始化,并且做到线程安全,下面会开始介绍
2.2. 懒汉:方法直接加同步(不要用)
最简单粗暴的方式,获取实例的方法直接上锁:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
确实线程安全了。
但是 synchronized 是互斥锁,悲观锁,getInstance 被调用频繁的情况下性能低下。
这样吧,我们把锁的粒度
2.3. 懒汉:双重检查锁定(推荐,记得 volatile)
双重检查锁定,本质是对锁的一个粒度优化。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
只有判断实例为空,才会进入同步代码。实例存在的话就直接返回了。
所以只有在创建的那一刹那,并且有多线程并发,会有一个互斥等待的过程。
因为进入同步代码,有可能其他线程已经实例化完毕,所以还要再检查一下是否已经实例化(判空),这就是双重检查。
因为 JVM 的指令优化,指令重排序现象会导致对象延迟初始化,所以其他线程读到实例不为空的时候,可能还没初始化。
所以需要加个 volatile
关键字,禁止重排序优化。
2.4. 懒汉:静态内部类(非常推荐)
这是一个比较机智的做法,利用 JVM 类加载机制,来延迟初始化对象实例
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
什么时候会进行类加载?
这里的静态内部类,外部的 Singleton
加载的时候并不会引起它的加载。因为虽然是它的内部静态类,但编译成字节码文件后是两个单独的类。
在调用 getInstance 方法后,静态内部类被主动调用,触发类加载流程。
而类加载流程是天然同步的,我们可以从源码上看到:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
...
return c;
}
}
我们也就不需要再进行加锁了。
2.5. 饿汉:静态工厂方法
在单例类加载的时候,马上实例化单例对象。
public class Singleton {
private Singleton() {}
private static final Singleton singleton = new Singleton();
public static Singleton getInstance() {
return singleton;
}
}
始终是线程安全的。
缺点是,及时没有调用该单例,也会在类使用的时候一开始就创建好了,在一些对性能要求高的场景会消耗性能。(这个在客户端场景比较常见)
2.6. 枚举实现
public enum Singleton {
INSTANCE;
private Singleton() {
}
}
创建枚举默认是线程安全的
3. 应用
什么时候需要使用单例?
控制实例的访问,所有的访问必须在单一实例上进行。
控制资源的使用,和线程池、连接池等配合使用,资源型单例,避免创建多个池导致资源的浪费。
控制对象的创建,不需要重复创建的对象,和工厂模式配合,比如实现工厂实例的唯一。
在客户端环境,比如 Android 开发,因为对应用启动的性能要求高,不希望应用一加载马上进行单例实例化,所以对懒汉模式应用较多。
3.1. JDK:Runtime
Runtime 可以获取 JVM 运行环境的信息。
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
...
}
4. 特点
4.1. 优势
- 中心化:控制所有的访问在唯一实例上进行
- 性能优化:避免频繁创建和销毁对象带来的性能损耗
- 内存优化:避免可共享资源的重复创建
4.2. 缺点
-
职责膨胀:如果承担的职责过多,违背单一职责原则
优化思路
把职责再重新剥离出去,或者与其他设计模式一起使用,单例仅作为入口
-
内存泄漏:如果成员变量引用了本来该释放的对象,引起泄漏,进而导致 OOM(内存溢出)
优化思路
第一,注意引用对象的生命周期
第二,如果确定要引用,需要有良好的内存释放机制。比如该成员变量为缓存池,考虑使用弱引用或者软引用,在虚拟机垃圾回收阶段或者内存紧张的时候进行对象的回收
4.3. 注意事项
- 不要滥用:实例和进程生命周期一致,长期不使用白白占用内存
- 不要反射:发射生成实例,会破坏单例的设计。(之前有遇到坑,继承的第三方服务反射一个单例类导致不唯一)
- 注意引用:成员变量尽量不去持有生命周期短的对象,非要持久需要注意释放机制
- 注意性能:根据业务来决定是否需要延迟初始化(用来抉择懒汉还是饿汉)
- 注意线程安全:使用懒汉模式,注意线程安全避免产生多个实例