今天就用通俗易懂的故事,来给您讲解下单例模式,并附上Java代码实现和优缺点分析。
故事解说单例模式
想象一下,你是一家公司的老板,公司里有一个非常重要的角色——财务总监。这位财务总监负责管理公司的所有财务事务,比如发工资、做账、报税等等。由于财务工作非常重要且敏感,你肯定不希望公司里有多个财务总监,各自为政,这样很容易造成混乱,甚至可能导致公司财务出现问题。
所以,你作为老板,会怎么做呢?你肯定会确保公司里只有一个财务总监,并且大家都只能通过这个唯一的财务总监来办理财务相关的事情。
在软件开发中,我们也经常遇到类似的情况。有些类在整个应用程序中只需要一个实例,比如:
- 日志记录器:负责记录应用程序的运行日志,只需要一个实例来统一管理日志的写入和输出。
- 数据库连接池:管理数据库连接,避免频繁地创建和关闭数据库连接,提高性能,只需要一个实例来管理所有的数据库连接。
- 配置管理器:读取和管理应用程序的配置信息,只需要一个实例来确保配置信息的一致性。
这时候,我们就可以使用单例模式来解决这个问题。单例模式确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。就像公司里只有一个财务总监,大家都通过这个唯一的财务总监来办理财务事务一样。
Java实现常用单例模式
下面,我用Java代码来实现几种常用的单例模式,并讲解它们的优缺点。
1. 懒汉式(Lazy Initialization)
故事场景:财务总监平时可能在休息,只有当有人需要办理财务事务时,才会去“唤醒”他,让他开始工作。
java
public class LazySingleton {
// 1. 私有静态变量,保存唯一实例,初始化为null
private static LazySingleton instance;
// 2. 私有构造函数,防止外部直接实例化
private LazySingleton() {}
// 3. 公共静态方法,提供全局访问点,按需创建实例
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
优点:
- 按需加载:实例在第一次被使用时才创建,节省了资源,提高了程序的启动速度。
缺点:
- 线程不安全:在多线程环境下,如果多个线程同时调用
getInstance()方法,可能会创建多个实例,破坏单例模式。
2. 饿汉式(Eager Initialization)
故事场景:财务总监在公司刚成立时就已到位,随时准备为大家服务。
java
public class EagerSingleton {
// 1. 私有静态变量,保存唯一实例,在类加载时就初始化
private static final EagerSingleton instance = new EagerSingleton();
// 2. 私有构造函数,防止外部直接实例化
private EagerSingleton() {}
// 3. 公共静态方法,提供全局访问点
public static EagerSingleton getInstance() {
return instance;
}
}
优点:
- 线程安全:由于实例在类加载时就创建了,而类加载过程是线程安全的,所以饿汉式单例是线程安全的。
- 实现简单:代码简单易懂,实现起来非常容易。
缺点:
- 资源浪费:即使实例从未被使用,也会在类加载时就创建,可能造成资源浪费,尤其是当实例创建成本较高时。
- 启动速度可能受影响:如果实例的初始化过程比较耗时,可能会影响程序的启动速度。
3. 双重检查锁定(Double-Checked Locking)
故事场景:财务总监大部分时间在休息,但当有人需要办理财务事务时,会先检查一下财务总监是否在岗,如果不在岗,才会去“唤醒”他,并且为了防止多人同时去“唤醒”,会加个“锁”,确保只有一个“唤醒”操作。
java
public class DoubleCheckedLockingSingleton {
// 1. 私有静态变量,保存唯一实例,使用volatile关键字保证可见性
private static volatile DoubleCheckedLockingSingleton instance;
// 2. 私有构造函数,防止外部直接实例化
private DoubleCheckedLockingSingleton() {}
// 3. 公共静态方法,提供全局访问点,双重检查锁定
public static DoubleCheckedLockingSingleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (DoubleCheckedLockingSingleton.class) { // 加锁
if (instance == null) { // 第二次检查(有锁)
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}
}
优点:
- 按需加载:实例在第一次被使用时才创建,节省了资源。
- 线程安全:通过双重检查锁定机制,保证了线程安全。
- 性能较好:只有在第一次创建实例时需要加锁,后续访问不需要加锁,性能较好。
缺点:
- 代码复杂度略高:相对于懒汉式和饿汉式,代码稍微复杂一些,理解起来需要一些时间。
- volatile关键字的必要性:必须使用
volatile关键字来修饰实例变量,否则可能由于指令重排序导致问题。
4. 静态内部类(Static Inner Class)
故事场景:财务总监平时在一个“独立办公室”里休息,这个“独立办公室”只有当有人需要办理财务事务时才会被“打开”,财务总监才会开始工作。
java
public class StaticInnerClassSingleton {
// 1. 私有构造函数,防止外部直接实例化
private StaticInnerClassSingleton() {}
// 2. 静态内部类,负责实例的创建
private static class SingletonHolder {
// 3. 静态常量,保存唯一实例,在SingletonHolder类加载时初始化
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
// 4. 公共静态方法,提供全局访问点,通过静态内部类获取实例
public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
优点:
- 按需加载:实例在第一次被使用时才创建(当
getInstance()方法第一次被调用时,静态内部类SingletonHolder才会被加载,实例才会被创建),节省了资源。 - 线程安全:利用了类加载机制来保证线程安全,JVM在类加载过程中会保证线程安全。
- 实现简单:代码简洁易懂,实现起来相对简单。
缺点:
- 反射攻击:理论上,可以通过反射机制破坏单例模式,创建多个实例(虽然在实际开发中这种情况比较少见,但需要注意)。
5. 枚举(Enum)
故事场景:财务总监是公司里一个“特殊职位”,这个职位只能有一个人担任,而且这个职位是“天生”就存在的,不需要去“创建”或“唤醒”。
java
public enum EnumSingleton {
INSTANCE; // 枚举实例,天生就是单例
// 可以在这里添加单例类需要的方法和字段
public void doSomething() {
System.out.println("EnumSingleton is doing something.");
}
}
优点:
- 线程安全:枚举实例的创建是线程安全的,由JVM保证。
- 防止反射攻击:枚举类型可以有效地防止通过反射机制创建多个实例,保证了单例的唯一性。
- 序列化安全:枚举类型在序列化和反序列化时,也能保证单例的唯一性。
- 实现简单:代码非常简洁,一行代码就能实现单例。
缺点:
- 灵活性较低:枚举类型不能继承其他类(只能实现接口),在某些需要继承的场景下可能不太适用。
- 不太直观:对于不熟悉枚举的人来说,可能不太容易理解枚举如何实现单例模式。
总结
| 单例模式 | 线程安全 | 按需加载 | 实现难度 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 懒汉式 | 否 | 是 | 简单 | 按需加载,节省资源 | 线程不安全,多线程环境下可能创建多个实例 |
| 饿汉式 | 是 | 否 | 简单 | 线程安全,实现简单 | 资源浪费,可能造成资源浪费,启动速度可能受影响 |
| 双重检查锁定 | 是 | 是 | 中等 | 按需加载,线程安全,性能较好 | 代码复杂度略高,需要volatile关键字 |
| 静态内部类 | 是 | 是 | 简单 | 按需加载,线程安全,实现简单 | 反射攻击(理论上) |
| 枚举 | 是 | 是 | 简单 | 线程安全,防止反射攻击,序列化安全,实现简单 | 灵活性较低,不太直观 |
在实际开发中,可以根据具体的需求和场景,选择最合适的单例模式实现方式。如果对线程安全和防止反射攻击有较高要求,推荐使用枚举或静态内部类方式实现单例模式。如果对按需加载和性能有较高要求,并且可以接受稍复杂的代码,可以选择双重检查锁定方式。