🔥 核心
单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点。
饿汉很饿,在类加载时就实例化;懒汉很懒,第一次调用时才初始化。
🙁 问题场景
假如你是上帝,你的手边有一个 Sun(太阳)
类的图纸。这时你突然犯了难?
天空中同时放几个太阳呢 —— 一个是不是太少?九个是不是太多?
每天升起的必须是一个太阳吗 —— 日复一日用这一个太阳?还是每天升起一个全新的太阳呢?
🙂 解决方案
显然,太阳的只有一个。
这就是一个典型的单例模式 —— 无论你是谁,无论你在何时何地抬头,天空中只有一个太阳,且永远是那个太阳。
🌈 有趣的例子
单例模式的实现思路多种多样(下面会一一介绍),但是它们都有着几个相同的步骤:
1)在类中添加一个私有静态成员变量用于保存唯一实例
2)将默认构造方法设置为私有,这样它就不能被 new
了
3)写一个公有静态成员方法,暴露给外部用于获取唯一实例
下面,我们来瞧一瞧单例模式有哪些巧妙的实现思路吧!
饿汉式
关键:饿汉很饿,在类加载时就实例化
描述:类加载时就初始化,非Lazy-Loading,可能会产生大量用不到的垃圾对象造成内存浪费;没加锁,所以效率高
补充:类加载机制也解决了多线程问题
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
懒汉式
关键:懒汉很懒,第一次调用时才初始化
描述:第一次调用时才初始化,即Lazy-Loading,避免了内存的浪费;加了锁才能保证单例,但加锁会影响效率
补充:99%的情况用不到synchronized同步,所以加锁是个极大的浪费
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双检锁(懒汉式的一种优化)
关键:采用双锁机制,既支持了多线程又保证了性能
描述:为什么要进行第二次判空?第一次判空时,可能有多个线程都判定instance为空,然后在锁外等待;之后它们进入锁后,不知道instance已经被刚刚进入的其他线程初始化过了,又会去重复初始化
补充:volatile关键字是为了防止指令重排
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
静态内部类(饿汉式的一种优化)
关键:使用静态内部类,实现了静态域的延迟初始化
描述:原理很简单,我们逆推一下逻辑:不调用getInstance(),就访问不到静态内部类;访问不到静态内部类,该类就不会加载,它内部的INSTANCE实例就不会被初始化。
补充:同样是借助类加载机制解决了多线程问题
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举
关键:借助枚举类的特性,天然解决单例问题
描述:写法简单,无须多言。
补充:什么?不熟悉枚举类?快去看看啊!
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("doSomething~");
}
}
☘️ 使用场景
◾️ 如果程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式。
单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。该方法可以创建一个新对象,但如果该对象已经被创建, 则返回已有的对象。
◾️如果你需要更加严格地控制全局变量,可以使用单例模式。
单例模式与全局变量不同,它保证类只存在一个实例。除了单例类自己以外,无法通过任何方式替换缓存的实例。
请注意,你可以随时调整限制并设定生成单例实例的数量,只需修改获取实例方法,即 getInstance
中的代码即可实现。
🧊 实现方式
(1)在类中添加一个私有静态成员变量用于保存单例实例。
(2)声明一个公有静态构建方法用于获取单例实例。
(3)在静态方法中实现"延迟初始化"。该方法会在首次被调用时创建一个新对象,并将其存储在静态成员变量中。此后该方法每次被调用时都返回该实例。
(4)将类的构造函数设为私有。类的静态方法仍能调用构造函数,但是其他对象不能调用。
(5)检查客户端代码,将对单例的构造函数的调用替换为对其静态构建方法的调用。
🎲 优缺点
➕ 你可以保证一个类只有一个实例。
➕ 你获得了一个指向该实例的全局访问节点。
➕ 仅在首次请求单例对象时对其进行初始化。
➕ 减少了内存的开销,尤其是频繁的创建和销毁实例。
➖ 没有接口,不能继承。
➖ 违反了单一职责原则。该模式同时解决了两个问题。
➖ 单例模式可能掩盖不良设计,比如程序各组件之间相互了解过多等。
➖ 该模式在多线程环境下需要进行特殊处理,避免多个线程多次创建单例对象。
➖ 单例的客户端代码单元测试可能会比较困难,因为许多测试框架以基于继承的方式创建模拟对象。
🌸 补充
饿汉式、懒汉式、双检锁、静态内部类实现的单例模式,都可以被反射或序列化破解(戳这里)。
但枚举不会,这也是我为什么极力推荐通过枚举实现单例模式,即使你不熟悉它。