单例模式是一种创建型设计模式,其核心目标是确保一个类在整个应用程序的生命周期中只有一个实例,并提供一个全局的访问点来获取这个唯一实例。这种模式常用于管理共享资源,如数据库连接池、线程池、日志记录器或配置管理器,以避免创建多个实例造成资源浪费或状态不一致。
单例模式的实现方式
Java中实现单例模式有多种方式,它们在线程安全性 、延迟加载(懒加载) 和实现复杂度上各有不同。下表对比了几种主要的实现方式:
| 实现方式 | 线程安全 | 延迟加载 | 实现复杂度 | 主要特点 |
|---|---|---|---|---|
| 饿汉式 | 是 | 否 | 简单 | 类加载时即初始化,绝对线程安全,但可能造成资源浪费。 |
| 懒汉式(基础版) | 否 | 是 | 简单 | 调用时创建实例,但多线程下会创建多个实例。 |
| 同步方法懒汉式 | 是 | 是 | 简单 | 通过synchronized方法保证线程安全,但每次访问都同步,性能差。 |
| 双重检查锁(DCL) | 是 | 是 | 中等 | 使用volatile和双重检查,兼顾线程安全和性能。 |
| 静态内部类 | 是 | 是 | 中等 | 利用类加载机制保证线程安全,且实现优雅的懒加载。 |
| 枚举 | 是 | 否 | 简单 | 由JVM保证单例,且能防止反射和序列化破坏,最安全。 |
- 饿汉式 (Eager Initialization)
实例在类加载时就创建,基于类加载机制保证线程安全。
java
public class EagerSingleton {
// 1. 私有静态常量实例
private static final EagerSingleton INSTANCE = new EagerSingleton();
// 2. 私有构造方法
private EagerSingleton() {}
// 3. 公共静态方法提供全局访问点
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
优点 :实现简单,线程安全绝对可靠。
缺点:无论是否使用,实例都已创建,可能造成内存浪费。
- 懒汉式 (Lazy Initialization) 及线程安全改进
基础懒汉式(非线程安全)
java
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) { // 线程A和B可能同时进入这里
instance = new LazySingleton(); // 导致创建多个实例
}
return instance;
}
}
此方式在多线程环境下会破坏单例。
同步方法懒汉式(线程安全但性能低)
java
public class SynchronizedLazySingleton {
private static SynchronizedLazySingleton instance;
private SynchronizedLazySingleton() {}
public static synchronized SynchronizedLazySingleton getInstance() {
if (instance == null) {
instance = new SynchronizedLazySingleton();
}
return instance;
}
}
通过synchronized关键字锁住整个方法保证线程安全,但每次调用都需同步,严重影响性能。
双重检查锁 (Double-Checked Locking, DCL)
这是对同步方法懒汉式的优化,旨在减少同步开销。
java
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();
// new DCLSingleton() 非原子操作,可能发生指令重排
}
}
}
return instance;
}
}
volatile关键字在此至关重要。instance = new DCLSingleton(); 这行代码并非原子操作,它大致分为三步:1) 分配内存空间;2) 初始化对象;3) 将引用指向内存地址。如果没有volatile,JVM可能进行指令重排,导致步骤3在步骤2之前执行。此时另一个线程在第一次检查时可能看到一个非null但未完全初始化的对象,从而引发程序错误。
- 静态内部类 (Static Inner Class)
利用Java类加载机制实现线程安全的懒加载,是一种优雅且高效的实现方式。
java
public class InnerClassSingleton {
private InnerClassSingleton() {}
// 静态内部类
private static class SingletonHolder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
// 只有当调用此方法时,SingletonHolder类才会被加载,从而初始化INSTANCE
return SingletonHolder.INSTANCE;
}
}
优点:无需同步锁,由JVM保证类加载过程的线程安全性,实现了延迟加载且性能高。
- 枚举 (Enum)
《Effective Java》作者Joshua Bloch推荐的方式,是实现单例的最佳实践。
java
public enum EnumSingleton {
INSTANCE; // 唯一的实例
// 可以添加实例方法
public void doSomething() {
System.out.println("Singleton operation.");
}
}
// 使用方式:EnumSingleton.INSTANCE.doSomething();
优点:
- 绝对防止多实例:JVM从根本上保证枚举常量只被实例化一次。
- 防止反射攻击:反射机制不能通过构造函数创建枚举实例。
- 防止序列化破坏:Java规范保证枚举类型的序列化和反序列化只会返回同一个实例。
- 线程安全:枚举的初始化由JVM在类加载时完成。
缺点:无法实现延迟加载。
常见问题与解决方案
-
反射攻击:通过反射调用私有构造器可以创建新的实例,破坏单例。
- 解决方案 :在私有构造器中加入判断,如果实例已存在则抛出异常。但最有效的防御是使用枚举单例,因为JVM禁止反射创建枚举实例。
javaprivate LazySingleton() { if (instance != null) { throw new RuntimeException("Use getInstance() method to get the single instance."); } } -
序列化与反序列化:将一个单例对象序列化后再反序列化,会得到一个新的对象。
- 解决方案 :实现
Serializable接口的类中,添加readResolve()方法,直接返回单例实例。同样,枚举单例天然免疫此问题。
javaprotected Object readResolve() { return getInstance(); } - 解决方案 :实现
-
多类加载器环境:如果同一个类被不同的类加载器加载,每个类加载器命名空间中都会有一个独立的类实例,导致单例失效。
- 解决方案:确保在同一个类加载器上下文(如Web应用的同一个WebAppClassLoader)中使用单例,或使用上下文类加载器进行控制。
总结与选择建议
- 简单直接,不考虑内存和懒加载 :选择饿汉式。
- 需要懒加载,且对性能有要求 :选择静态内部类 或双重检查锁(DCL)。静态内部类实现更简洁,通常更受青睐。
- 追求极致的安全性和简洁性,无需懒加载 :强烈推荐使用枚举方式,它能有效防御反射和序列化的攻击,是实现单例最安全、最简洁的方法。