1. 什么是单例模式
单例模式(Singleton Pattern)是 Java 中最基础、最常用的创建型设计模式之一。它的核心思想是:保证一个类在整个应用程序生命周期中只有一个实例,并提供一个全局访问点。
为什么需要单例?在实际开发中,有些对象只需要一个实例就够了,比如:
- 数据库连接池
- 线程池
- 配置管理器
- 日志记录器
- 缓存管理器
如果这些对象被反复创建,不仅浪费内存资源,还可能导致数据不一致、连接耗尽等问题。
2. 单例模式的实现方式
单例模式的实现方式有很多种,每种方式在线程安全、性能、延迟加载等方面各有取舍。下面逐一介绍。
2.1 饿汉式(Eager Initialization)
饿汉式在类加载时就创建实例,实现最简单,天然线程安全。
java
public class EagerSingleton {
// 类加载时即创建实例
private static final EagerSingleton INSTANCE = new EagerSingleton();
// 私有构造方法,防止外部 new
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
优点 :实现简单,线程安全,无需加锁。
缺点:类加载时就创建实例,即使从未使用也会占用内存,造成资源浪费。
2.2 懒汉式(Lazy Initialization)
懒汉式在第一次调用 getInstance() 时才创建实例,实现了延迟加载。
java
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
优点 :延迟加载,节省资源。
缺点 :线程不安全 。多个线程同时调用 getInstance() 时,可能创建多个实例。
2.3 线程安全的懒汉式(同步方法)
在 getInstance() 方法上加 synchronized 关键字,保证线程安全。
java
public class SynchronizedSingleton {
private static SynchronizedSingleton instance;
private SynchronizedSingleton() {}
public static synchronized SynchronizedSingleton getInstance() {
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
优点 :线程安全。
缺点 :每次调用 getInstance() 都要加锁,性能开销大。实际上只有第一次创建实例时才需要同步。
2.4 双重检查锁(Double-Checked Locking)
双重检查锁结合了懒加载和线程安全,是懒汉式的优化版本。
java
public class DoubleCheckedSingleton {
// volatile 防止指令重排序
private static volatile DoubleCheckedSingleton instance;
private DoubleCheckedSingleton() {}
public static DoubleCheckedSingleton getInstance() {
if (instance == null) { // 第一次检查(不加锁)
synchronized (DoubleCheckedSingleton.class) {
if (instance == null) { // 第二次检查(加锁)
instance = new DoubleCheckedSingleton();
}
}
}
return instance;
}
}
关键点 :instance 必须用 volatile 修饰。因为 new DoubleCheckedSingleton() 不是原子操作,分为三步:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
如果没有 volatile,JVM 可能对第 2、3 步进行指令重排序,导致其他线程拿到一个未初始化完成的对象。
优点 :延迟加载 + 线程安全 + 性能较好(只有第一次创建时才加锁)。
缺点 :实现稍复杂,需要理解 volatile 的原理。
2.5 静态内部类(推荐)
利用 JVM 的类加载机制保证线程安全,同时实现延迟加载,是最推荐的实现方式。
java
public class StaticInnerSingleton {
private StaticInnerSingleton() {}
// 静态内部类,只有被调用时才加载
private static class Holder {
private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
}
public static StaticInnerSingleton getInstance() {
return Holder.INSTANCE;
}
}
原理 :JVM 在加载 StaticInnerSingleton 时不会加载 Holder 内部类,只有第一次调用 getInstance() 时才会加载 Holder 并创建实例。JVM 保证类加载过程是线程安全的,因此无需额外加锁。
优点 :延迟加载 + 线程安全 + 实现简洁。
缺点:无法在创建实例时传入参数。
2.6 枚举方式(最简洁)
枚举是《Effective Java》作者 Joshua Bloch 推荐的实现方式,也是最简洁、最安全的单例实现。
java
public enum EnumSingleton {
INSTANCE;
// 可以添加业务方法
public void doSomething() {
System.out.println("枚举单例方法调用");
}
}
使用方式:
java
EnumSingleton.INSTANCE.doSomething();
优点:
- 实现最简单
- 天然线程安全
- 天然防止反射攻击和序列化破坏
缺点:无法实现延迟加载(枚举常量在类加载时创建)。
3. 单例模式的破坏与防护
单例模式虽然保证了构造方法私有,但仍可能被以下方式破坏:
3.1 反射破坏
通过反射可以绕过私有构造方法的限制:
java
Class<?> clazz = Singleton.class;
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance1 = (Singleton) constructor.newInstance();
Singleton instance2 = Singleton.getInstance();
// instance1 != instance2,单例被破坏
防护方案:在构造方法中增加判断,如果实例已存在则抛出异常。
java
public class SafeSingleton {
private static volatile SafeSingleton instance;
private SafeSingleton() {
if (instance != null) {
throw new RuntimeException("禁止通过反射创建实例");
}
}
}
3.2 序列化破坏
如果单例类实现了 Serializable 接口,反序列化时会创建新实例。
防护方案 :重写 readResolve() 方法。
java
public class SerializableSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private static final SerializableSingleton INSTANCE = new SerializableSingleton();
private SerializableSingleton() {}
public static SerializableSingleton getInstance() {
return INSTANCE;
}
// 反序列化时返回已有实例
protected Object readResolve() {
return INSTANCE;
}
}
4. 单例模式的应用场景
单例模式在真实项目中应用广泛,以下是几个典型场景:
4.1 数据库连接池
java
public class DatabaseConnectionPool {
private static volatile DatabaseConnectionPool pool;
private List<Connection> connections;
private DatabaseConnectionPool() {
// 初始化连接池
connections = new ArrayList<>();
for (int i = 0; i < 10; i++) {
connections.add(createConnection());
}
}
public static DatabaseConnectionPool getInstance() {
if (pool == null) {
synchronized (DatabaseConnectionPool.class) {
if (pool == null) {
pool = new DatabaseConnectionPool();
}
}
}
return pool;
}
public Connection getConnection() {
// 从池中获取连接
return connections.remove(0);
}
}
4.2 配置管理器
java
public class ConfigManager {
private static final ConfigManager INSTANCE = new ConfigManager();
private Properties properties;
private ConfigManager() {
properties = new Properties();
// 加载配置文件
try (InputStream in = getClass().getClassLoader()
.getResourceAsStream("config.properties")) {
properties.load(in);
} catch (IOException e) {
e.printStackTrace();
}
}
public static ConfigManager getInstance() {
return INSTANCE;
}
public String get(String key) {
return properties.getProperty(key);
}
}
5. 单例模式总结
| 实现方式 | 延迟加载 | 线程安全 | 性能 | 推荐度 |
|---|---|---|---|---|
| 饿汉式 | 否 | 是 | 高 | ⭐⭐⭐ |
| 懒汉式 | 是 | 否 | 高 | ⭐ |
| 同步方法 | 是 | 是 | 低 | ⭐⭐ |
| 双重检查锁 | 是 | 是 | 高 | ⭐⭐⭐⭐ |
| 静态内部类 | 是 | 是 | 高 | ⭐⭐⭐⭐⭐ |
| 枚举 | 否 | 是 | 高 | ⭐⭐⭐⭐⭐ |
选择建议:
- 如果实例一定会被使用,且创建开销不大,用饿汉式即可。
- 如果需要延迟加载,优先选择静态内部类,实现简洁且线程安全。
- 如果需要防止反射和序列化破坏,使用枚举。
- 面试中常考双重检查锁 ,需要理解
volatile的作用。
6. 常见面试问题
Q1:单例模式为什么要有私有构造方法?
防止外部通过 new 关键字创建实例,保证全局只有一个实例。
Q2:双重检查锁中 volatile 的作用是什么?
防止指令重排序。new 操作不是原子操作,volatile 保证其他线程不会读取到未初始化完成的对象。
Q3:静态内部类为什么是线程安全的?
JVM 保证类的加载过程是线程安全的,多个线程同时加载同一个类时,只有一个线程会执行类初始化,其他线程会等待。
Q4:枚举单例为什么能防止反射破坏?
枚举类的构造方法在 JVM 层面有特殊保护,反射调用 newInstance() 时会抛出 IllegalArgumentException。