单例设计模式详解
单例模式:懒汉式 vs 饿汉式
单例模式是Java中最常用的设计模式之一,确保一个类只有一个实例,并提供全局访问点。
其中懒汉式和饿汉式是两种最基础的实现方式,各有特点和适用场景。
一、饿汉式(Eager Initialization)
1. 核心特点
- 立即加载:在类加载时就创建实例
- 线程安全:由JVM类加载机制保证线程安全
- 资源利用率:可能造成资源浪费(如果实例未被使用)
2. 标准实现
public class EagerSingleton {// 1. 私有静态实例(类加载时立即创建)private static final EagerSingleton instance = new EagerSingleton();// 2. 私有构造方法private EagerSingleton() {}// 3. 公共静态获取方法public static EagerSingleton getInstance() {return instance;}
}
3. 关键点分析
static final
修饰确保实例唯一性和不可变性- 构造方法私有化防止外部实例化
- 没有同步开销,性能最佳
4. 适用场景
- 实例创建开销小
- 程序运行期间一定会用到该实例
- 对性能要求高的场景
二、懒汉式(Lazy Initialization)
1. 核心特点
- 延迟加载:只有第一次调用getInstance()时才创建实例
- 线程安全需要额外处理:基础实现是非线程安全的
- 资源利用率高:避免不必要的资源占用
2. 演进版本
(1) 基础版(非线程安全)
public class UnsafeLazySingleton {private static UnsafeLazySingleton instance;private UnsafeLazySingleton() {}public static UnsafeLazySingleton getInstance() {if (instance == null) { // 非原子操作instance = new UnsafeLazySingleton();}return instance;}
}
问题:多线程环境下可能创建多个实例
(2) 同步方法版(线程安全但性能差)
public class SynchronizedLazySingleton {private static SynchronizedLazySingleton instance;private SynchronizedLazySingleton() {}public static synchronized SynchronizedLazySingleton getInstance() {if (instance == null) {instance = new SynchronizedLazySingleton();}return instance;}
}
缺点:每次获取实例都要同步,性能瓶颈
(3) 双重检查锁(DCL,最优解)
public class DCLSingleton {// volatile保证可见性和禁止指令重排序private static volatile DCLSingleton instance;private DCLSingleton() {}public static DCLSingleton getInstance() {if (instance == null) { // 第一次检查synchronized (DCLSingleton.class) {if (instance == null) { // 第二次检查instance = new DCLSingleton();}}}return instance;}
}
优势:
- 只有第一次创建时需要同步
- volatile防止指令重排序导致的问题
- 性能接近饿汉式
(4) 静态内部类实现(推荐方案)
public class InnerClassSingleton {private InnerClassSingleton() {}private static class SingletonHolder {private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();}public static InnerClassSingleton getInstance() {return SingletonHolder.INSTANCE;}
}
原理:利用JVM类加载机制保证线程安全,只有调用getInstance()时才会加载静态内部类
三、对比总结
特性 | 饿汉式 | 懒汉式(DCL) | 懒汉式(静态内部类) |
---|---|---|---|
初始化时机 | 类加载时立即初始化 | 第一次调用时初始化 | 第一次调用时初始化 |
线程安全 | 天生安全 | 需要双重检查锁 | 天生安全 |
性能 | 最佳 | 接近饿汉式 | 接近饿汉式 |
资源占用 | 可能浪费 | 按需加载 | 按需加载 |
实现复杂度 | 最简单 | 较复杂 | 中等 |
防反射/反序列化 | 需要额外处理 | 需要额外处理 | 需要额外处理 |
四、面试常考点
-
为什么需要双重检查?
- 第一次检查:避免不必要的同步
- 第二次检查:防止多个线程通过第一次检查后重复创建实例
-
volatile关键字的作用?
- 保证可见性:确保所有线程看到最新的实例状态
- 禁止指令重排序:防止对象未初始化完成就被使用
-
静态内部类实现的原理?
- JVM保证类加载的线程安全性
- 延迟加载:只有访问静态内部类时才会触发其加载
-
如何防止反射破坏单例?
private Singleton() {if (instance != null) {throw new RuntimeException("禁止反射创建实例");} }
-
如何防止反序列化破坏单例?
private Object readResolve() {return getInstance(); }
五、实际应用建议
- 首选静态内部类实现:简洁、高效、线程安全
- 需要参数化初始化时用DCL
- 确定会立即使用的实例用 饿汉式
记住:在Java 1.5+环境下,双重检查锁实现必须使用volatile才能完全正确工作