在Java中,单例模式(Singleton Pattern)确保一个类只有一个实例,并提供全局访问点。以下是实现单例的五种常见方式:懒汉式、饿汉式、双重检查锁、静态内部类和枚举,包括代码示例和优缺点分析。
1. 懒汉式(Lazy Initialization)
- 特点:延迟加载,实例在第一次使用时创建。
- 线程安全:基本实现非线程安全,需加锁优化。
- 代码示例:
public class Singleton { private static Singleton instance; private Singleton() {} // 私有构造,防止外部实例化 public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
- 线程安全版(加锁):
public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
- 线程安全版(加锁):
- 优点:
- 延迟加载,节省内存(只有在使用时才创建实例)。
- 缺点:
- 基本版线程不安全。
- 线程安全版使用
synchronized
方法锁,性能较低(每次调用都加锁)。
- 适用场景:单线程环境或实例创建开销小、对性能要求不高的场景。
2. 饿汉式(Eager Initialization)
- 特点:类加载时就创建实例,急切初始化。
- 线程安全:天然线程安全,依赖JVM类加载机制。
- 代码示例:
public class Singleton { private static final Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }
- 优点:
- 实现简单,线程安全(由JVM类加载保证)。
- 无需加锁,性能较高。
- 缺点:
- 非延迟加载,类加载时即创建实例,可能浪费内存(如果实例未被使用)。
- 如果构造方法有复杂逻辑,类加载可能变慢。
- 适用场景:实例创建开销小、确定会被使用的场景。
3. 双重检查锁(Double-Checked Locking, DCL)
- 特点:结合懒汉式的延迟加载和线程安全,减少锁粒度。
- 线程安全:通过
volatile
和双重检查确保线程安全。 - 代码示例:
public class Singleton { 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; } }
- 关键点:
volatile
防止指令重排序,确保实例初始化完成前不被其他线程访问。- 双重检查减少锁竞争,仅在实例未创建时加锁。
- 优点:
- 延迟加载,节省内存。
- 线程安全,性能较高(锁粒度小)。
- 缺点:
- 实现较复杂,需理解
volatile
和指令重排序。 - 早期Java版本(1.4及之前)可能有DCL失效问题(现已解决)。
- 实现较复杂,需理解
- 适用场景:需要延迟加载且高并发的场景。
4. 静态内部类(Static Inner Class)
- 特点:利用JVM类加载机制实现延迟加载和线程安全。
- 线程安全:由JVM保证静态内部类加载时的线程安全。
- 代码示例:
public class Singleton { private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
- 原理:
- 静态内部类
SingletonHolder
在getInstance()
调用时才加载,触发INSTANCE
初始化。 - JVM确保类加载过程线程安全,无需额外同步。
- 静态内部类
- 优点:
- 延迟加载,节省内存。
- 线程安全,无需加锁,性能高。
- 实现优雅,代码简洁。
- 缺点:
- 无法传递参数给构造方法(除非通过其他方式)。
- 适用场景:需要延迟加载且要求高性能的场景,推荐使用。
5. 枚举(Enum)
- 特点:使用Java枚举类型实现单例,简洁且天然线程安全。
- 线程安全:由JVM保证枚举实例的单例性。
- 代码示例:
public enum Singleton { INSTANCE; // 可添加方法 public void doSomething() { System.out.println("Singleton method called"); } }
- 使用方式:
Singleton.INSTANCE.doSomething();
- 使用方式:
- 优点:
- 实现最简单,代码极少。
- 线程安全,由JVM保证。
- 防止反射攻击和序列化问题(枚举天生防止反序列化创建新实例)。
- 支持添加方法,功能灵活。
- 缺点:
- 非延迟加载,枚举类加载时即创建实例。
- 不支持复杂构造逻辑(枚举构造较为固定)。
- 适用场景:需要绝对线程安全、防止反射/序列化问题、逻辑简单的场景。
6. 对比总结
方式 | 延迟加载 | 线程安全 | 性能 | 复杂性 | 防止反射/序列化 | 适用场景 |
---|---|---|---|---|---|---|
懒汉式 | 是 | 否/是(加锁) | 低(锁) | 低 | 需额外处理 | 单线程或低并发 |
饿汉式 | 否 | 是 | 高 | 低 | 需额外处理 | 确定使用、无内存限制 |
双重检查锁 | 是 | 是 | 高 | 高 | 需额外处理 | 高并发、延迟加载 |
静态内部类 | 是 | 是 | 高 | 中 | 需额外处理 | 高性能、延迟加载,推荐 |
枚举 | 否 | 是 | 高 | 低 | 天然支持 | 简单逻辑、防反射/序列化,推荐 |
7. 注意事项
- 私有构造:所有实现都需私有构造方法,防止外部实例化。
- 反射攻击:除枚举外,其他方式可能通过反射创建实例,需在构造方法中加防护:
private Singleton() { if (instance != null) { throw new RuntimeException("Instance already exists!"); } }
- 序列化问题:除枚举外,单例实现序列化时需实现
readResolve
方法:private Object readResolve() { return instance; }
- Spring中的单例:Spring的单例是容器级别的,生命周期由Spring管理,通常无需手动实现单例模式。
8. 推荐方式
- 首选:静态内部类(延迟加载、线程安全、实现优雅)。
- 次选:枚举(最简单、防反射/序列化,适合简单场景)。
- 高并发:双重检查锁(需确保正确使用
volatile
)。 - 确定使用:饿汉式(简单直接)。
- 避免:懒汉式(除非单线程或加锁优化)。