一、单例模式核心思想
单例模式是一种创建型设计模式,其核心目标 是确保一个类只有一个实例,并提供一个全局访问点。该模式通过控制实例化过程来限制类的实例数量,在需要限制某些类的实例数量时非常有用。
单例模式的动机来源于实际软件开发中的常见需求。对于系统中的某些类来说,只有一个实例非常重要。例如,一个系统只能有一个窗口管理器或文件系统,一个应用程序只能有一个计时工具或ID生成器。
单例模式有三个关键要点:
-
某个类只能有一个实例
-
它必须自行创建这个实例
-
它必须自行向整个系统提供这个实例
在软件系统中,单例模式主要应用于需要控制资源的使用场景,如配置管理器、日志记录器、数据库连接池等,这些场景下确保实例唯一性可以避免资源浪费和数据不一致性问题。
二、饿汉式单例:急切而直接
2.1 实现原理
饿汉式单例是单例模式的一种直接实现方式,其核心特征是在类加载阶段就立即创建实例。这种方式得名于其"急切"的特性------就像饥饿的人急于获取食物一样,饿汉式单例在类被加载时就迫不及待地创建实例。
2.2 代码实现
/**
* 饿汉式单例实现
* 特点:类加载时立即初始化,线程安全
*/
public class EagerSingleton {
// 静态常量,在类加载时即完成实例化
private static final EagerSingleton INSTANCE = new EagerSingleton();
// 私有构造器,防止外部实例化
private EagerSingleton() {
// 防止通过反射创建实例
if (INSTANCE != null) {
throw new RuntimeException("单例模式禁止通过反射创建实例");
}
System.out.println("饿汉式单例实例被创建");
}
// 全局访问点
public static EagerSingleton getInstance() {
return INSTANCE;
}
// 示例方法
public void showMessage() {
System.out.println("Hello, I am an Eager Singleton!");
}
}
2.3 优缺点分析
优点:
-
线程安全:实例在类加载时创建,JVM保证线程安全
-
实现简单:代码简洁,易于理解和维护
-
性能高效:获取实例时无需同步,调用速度快
缺点:
-
资源浪费:即使不使用该实例,也会在启动时创建,占用内存
-
启动缓慢:如果实例初始化复杂,可能拖慢应用启动速度
-
灵活性差:无法实现延迟加载,无法根据参数创建实例
三、懒汉式单例:懒惰而谨慎
3.1 实现原理
懒汉式单例在首次请求时才创建实例,实现了延迟加载。这种方式需要在多线程环境下特别处理线程安全问题。
3.2 基础实现(线程不安全)
/**
* 基础懒汉式单例(线程不安全)
* 仅适用于单线程环境
*/
public class UnsafeLazySingleton {
private static UnsafeLazySingleton instance;
private UnsafeLazySingleton() {
System.out.println("基础懒汉式单例实例被创建");
}
public static UnsafeLazySingleton getInstance() {
if (instance == null) {
instance = new UnsafeLazySingleton(); // 多线程下可能创建多个实例
}
return instance;
}
}
3.3 线程安全实现
/**
* 同步方法懒汉式单例
* 线程安全但效率较低
*/
public class SynchronizedLazySingleton {
private static SynchronizedLazySingleton instance;
private SynchronizedLazySingleton() {
System.out.println("同步懒汉式单例实例被创建");
}
// 使用synchronized保证线程安全
public static synchronized SynchronizedLazySingleton getInstance() {
if (instance == null) {
instance = new SynchronizedLazySingleton();
}
return instance;
}
}
3.4 双重检查锁定(Double-Checked Locking)
/**
* 双重检查锁定懒汉式单例
* 兼顾线程安全和性能
*/
public class DoubleCheckedLockingSingleton {
// 使用volatile防止指令重排序
private static volatile DoubleCheckedLockingSingleton instance;
private DoubleCheckedLockingSingleton() {
System.out.println("双重检查锁定单例实例被创建");
}
public static DoubleCheckedLockingSingleton getInstance() {
// 第一次检查,避免不必要的同步
if (instance == null) {
synchronized (DoubleCheckedLockingSingleton.class) {
// 第二次检查,确保只有一个线程创建实例
if (instance == null) {
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}
}
3.5 静态内部类实现(推荐)
/**
* 静态内部类懒汉式单例(推荐)
* 既实现了延迟加载,又保证了线程安全
*/
public class InnerClassLazySingleton {
private InnerClassLazySingleton() {
System.out.println("静态内部类懒汉式单例实例被创建");
}
// 静态内部类在第一次使用时才会加载
private static class SingletonHolder {
private static final InnerClassLazySingleton INSTANCE = new InnerClassLazySingleton();
}
public static InnerClassLazySingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
3.6 优缺点分析
优点:
-
延迟加载:只有在需要时才创建实例,节省资源
-
灵活性高:可以根据运行时条件创建实例
-
优化启动速度:不会拖慢应用启动时间
缺点:
-
实现复杂:需要考虑线程安全问题
-
性能开销:同步机制可能带来性能损失
-
潜在风险:错误的实现可能导致多实例或空指针异常
四、饿汉式与懒汉式对比
| 对比维度 | 饿汉式 | 懒汉式 |
|---|---|---|
| 实例化时机 | 类加载时立即创建 | 第一次使用时创建 |
| 线程安全 | 天然线程安全 | 需要额外处理 |
| 资源占用 | 可能浪费内存 | 按需使用内存 |
| 性能表现 | 获取实例快 | 首次获取可能慢 |
| 实现复杂度 | 简单直接 | 相对复杂 |
| 适用场景 | 实例小且必用 | 实例大或不常用 |
五、实战选择建议
5.1 何时选择饿汉式
-
实例较小:创建实例开销不大
-
必定使用:应用运行期间一定会用到该实例
-
启动速度不敏感:应用启动时间要求不高
-
简单场景:不想引入复杂的同步逻辑
// 配置文件管理器适合使用饿汉式
public class ConfigManager {
private static final ConfigManager INSTANCE = new ConfigManager();
private Properties config;private ConfigManager() { loadConfig(); } private void loadConfig() { // 加载配置文件 } public static ConfigManager getInstance() { return INSTANCE; }}
5.2 何时选择懒汉式
-
实例较大:创建实例需要大量资源
-
可能不用:应用可能在某些情况下不需要该实例
-
启动速度敏感:需要快速启动应用
-
依赖参数:实例创建需要运行时参数
// 数据库连接池适合使用懒汉式
public class ConnectionPool {
private static volatile ConnectionPool instance;
private List<Connection> connections;private ConnectionPool(int poolSize) { initConnections(poolSize); } public static ConnectionPool getInstance(int poolSize) { if (instance == null) { synchronized (ConnectionPool.class) { if (instance == null) { instance = new ConnectionPool(poolSize); } } } return instance; }}
六、现代Java中的单例模式
6.1 枚举实现(最佳实践)
/**
* 枚举单例(推荐)
* Joshua Bloch在《Effective Java》中推荐的方法
* 天然防止反射攻击和序列化问题
*/
public enum EnumSingleton {
INSTANCE;
// 可以添加方法和属性
private String data;
public void setData(String data) {
this.data = data;
}
public String getData() {
return data;
}
public void doSomething() {
System.out.println("枚举单例执行操作");
}
}
// 使用方式
EnumSingleton.INSTANCE.doSomething();
6.2 使用框架管理单例
// Spring框架中的单例(默认作用域)
@Component
public class SpringSingletonService {
// Spring容器管理单例生命周期
}
七、常见问题与注意事项
-
反射攻击防护:通过私有构造器中检查实例是否已存在
-
序列化安全 :实现
readResolve()方法防止反序列化创建新实例 -
克隆安全 :重写
clone()方法并抛出异常 -
内存泄漏:单例持有大对象时注意及时释放
八、总结
饿汉式和懒汉式单例各有适用场景,选择哪种实现取决于具体需求:
-
追求简单和性能 → 选择饿汉式
-
追求资源节约和灵活性 → 选择懒汉式
-
追求完美和安全性 → 选择枚举实现
在现代Java开发中,如果必须手写单例,静态内部类实现 和枚举实现是最推荐的方式。但更常见的情况是,我们使用Spring等框架的容器管理单例,这既保证了单例的特性,又避免了手写实现的潜在问题。
无论选择哪种方式,理解单例模式的原理和各种实现的优缺点,有助于我们在实际开发中做出合理的设计决策。