Java 单例模式(Singleton)
一、什么是单例
保证一个类在整个程序中,永远只有唯一一个实例对象,全局共用这一个对象。
适用场景:工具类、数据库连接池、线程池、配置管理器等,频繁创建销毁浪费资源。
二、核心实现要点
- 私有构造方法
private:禁止外部new创建对象 - 类内部持有唯一静态实例
- 提供静态方法,对外获取该唯一实例
三、常见 4 种写法
1. 饿汉式(最简单,线程安全)
类加载时直接创建实例,天生线程安全,缺点:类一加载就占用内存,不用也会创建。
java
public class Singleton {
// 类加载直接初始化
private static final Singleton INSTANCE = new Singleton();
// 私有构造,禁止new
private Singleton(){}
// 对外获取实例
public static Singleton getInstance(){
return INSTANCE;
}
}
使用:Singleton s = Singleton.getInstance();
2. 懒汉式(非线程安全,不推荐)
调用方法时才创建,延迟加载;多线程并发会创建多个对象。
java
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
3. 双重检查锁 DCL(Double-Check Locking,工作常用)
延迟加载 + 线程安全,性能好。
必须加 volatile 防止指令重排导致空指针。
java
public class Singleton {
// volatile 禁止指令重排
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;
}
}
4. 静态内部类(最优写法,推荐)
懒加载、天然线程安全、无锁、性能高。
外部类加载不会初始化内部类,只有调用 getInstance() 才创建对象。
java
public class Singleton {
private Singleton(){}
// 静态内部类
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
5. 枚举单例(最安全,防反射、序列化破坏)
Effective Java 推荐,彻底杜绝反射、序列化破解单例。
java
public enum Singleton {
INSTANCE;
// 业务方法
public void doSomething(){
System.out.println("单例执行");
}
}
// 使用
Singleton.INSTANCE.doSomething();
四、破坏单例的两种方式 & 规避
- 反射破坏
反射可以调用私有构造创建新对象。
解决:枚举单例 / 构造中判断实例非空直接抛异常。
java
private Singleton(){
if(instance != null){
throw new RuntimeException("禁止反射创建实例");
}
}
- 序列化破坏
对象序列化再反序列化会生成新对象。
解决:添加readResolve()方法返回已有实例。
java
private Object readResolve(){
return instance;
}
五、各方式对比总结
| 写法 | 懒加载 | 线程安全 | 优点 | 缺点 |
|---|---|---|---|---|
| 饿汉式 | ❌ | ✅ | 简单安全 | 提前占用内存 |
| 普通懒汉 | ✅ | ❌ | 延迟加载 | 多线程不安全 |
| DCL双重检查 | ✅ | ✅ | 延迟、高性能 | 需volatile,写法复杂 |
| 静态内部类 | ✅ | ✅ | 简洁、无锁、高效 | 无法传参构造 |
| 枚举 | ✅ | ✅ | 防反射序列化,极简 | 不支持有参构造 |
六、面试高频问题
- 为什么 DCL 要加 volatile?
new 对象分三步:分配内存→初始化对象→赋值引用。volatile 禁止指令重排,避免其他线程拿到半初始化对象。 - 静态内部类为什么线程安全?
静态变量初始化由 JVM 保证同步,只会执行一次。 - 为什么枚举是最安全单例?
底层禁止反射调用构造,序列化不会生成新实例,无漏洞。