👉 更多 文章、资料、干货 ,尽在个人主页!点击头像,获取更多~ 📚
我们将深入探讨 Java 设计模式中最为基础也最为重要的一种------单例设计模式。这不仅仅是一个模式,它关乎程序的性能、资源的合理利用以及线程安全的核心原则。
无论你是初学者还是经验丰富的开发者,这篇详尽的指南都将为你提供深入的见解和实用的代码示例。
📚 什么是单例模式?
单例模式属于创建型模式 ,其核心目标是确保一个类在 JVM 中只有一个实例。它提供了一种全局访问点来获取这个唯一的实例,而无需频繁地创建和销毁对象。
🎯 为什么需要单例模式?
- 资源控制: 某些资源(如数据库连接池、线程池、配置文件读取器)是有限的,创建多个实例会造成资源浪费和冲突。
- 性能优化: 频繁创建和销毁对象(特别是重量级对象)会带来性能开销。单例可以避免这种开销。
- 全局状态: 某些场景需要一个全局的、共享的状态管理器,单例是实现这一点的理想选择。
🏗️ 单例模式的结构
单例模式主要包含两个角色:
- 单例类 (Singleton Class): 负责创建并管理自身的唯一实例。它拥有一个私有的构造方法,防止外部直接实例化。
- 访问类 (Client Class): 通过单例类提供的公共静态方法来获取其唯一实例。
⚙️ 单例模式的实现方式
单例模式主要有两种实现思路:饿汉式 和懒汉式。它们的核心区别在于实例化时机。
1️⃣ 饿汉式 (Eager Initialization)
特点:在类加载时就创建实例,线程安全,但可能存在内存浪费。
方式一:静态变量方式
这是最直观的实现方式,实例在类加载时立即创建。
/**
* 饿汉式 - 静态变量方式
* 优点:写法简单,类加载时就完成了实例化,避免了线程同步问题。
* 缺点:容易造成内存浪费(无论是否使用,实例都会被创建)。
*/
public class EagerSingleton_StaticVariable {
// 1. 私有构造方法,防止外部 new
private EagerSingleton_StaticVariable() {
System.out.println("EagerSingleton_StaticVariable 构造方法被调用...");
}
// 2. 在类加载时就创建好唯一的实例
private static final EagerSingleton_StaticVariable INSTANCE = new EagerSingleton_StaticVariable();
// 3. 提供公共静态方法供外部获取实例
public static EagerSingleton_StaticVariable getInstance() {
return INSTANCE;
}
}
// --- 应用场景示例:数据库配置管理器 ---
class DatabaseConfigManager {
private String url;
private String username;
private String password;
private DatabaseConfigManager() {
// 从配置文件加载,或从环境变量读取
this.url = "jdbc:mysql://localhost:3306/mydb";
this.username = "admin";
this.password = "secret";
System.out.println("DatabaseConfigManager 初始化完成,加载配置信息。");
}
private static final DatabaseConfigManager INSTANCE = new DatabaseConfigManager();
public static DatabaseConfigManager getInstance() {
return INSTANCE;
}
public String getUrl() { return url; }
public String getUsername() { return username; }
public String getPassword() { return password; }
}
// --- 测试 ---
class EagerTest {
public static void main(String[] args) {
// 无论是否调用 getInstance,INSTANCE 都已经创建了
EagerSingleton_StaticVariable s1 = EagerSingleton_StaticVariable.getInstance();
EagerSingleton_StaticVariable s2 = EagerSingleton_StaticVariable.getInstance();
System.out.println("s1 == s2: " + (s1 == s2)); // 输出: true
// 使用配置管理器
DatabaseConfigManager config1 = DatabaseConfigManager.getInstance();
DatabaseConfigManager config2 = DatabaseConfigManager.getInstance();
System.out.println("Config1 URL: " + config1.getUrl());
System.out.println("Config1 == Config2: " + (config1 == config2)); // 输出: true
}
}
方式二:静态代码块方式
与方式一类似,但将实例创建放在了静态代码块中,这在需要进行一些复杂初始化操作时可能有用。
/**
* 饿汉式 - 静态代码块方式
* 优点:写法简单,类加载时就完成了实例化,避免了线程同步问题。
* 静态代码块中可以执行更复杂的初始化逻辑。
* 缺点:同样容易造成内存浪费。
*/
public class EagerSingleton_StaticBlock {
private EagerSingleton_StaticBlock() {
System.out.println("EagerSingleton_StaticBlock 构造方法被调用...");
}
private static final EagerSingleton_StaticBlock INSTANCE;
static { // 静态代码块,在类加载时执行一次
System.out.println("开始在静态代码块中创建实例...");
INSTANCE = new EagerSingleton_StaticBlock();
System.out.println("实例创建完成。");
}
public static EagerSingleton_StaticBlock getInstance() {
return INSTANCE;
}
}
// --- 测试 ---
class EagerBlockTest {
public static void main(String[] args) {
EagerSingleton_StaticBlock s1 = EagerSingleton_StaticBlock.getInstance();
EagerSingleton_StaticBlock s2 = EagerSingleton_StaticBlock.getInstance();
System.out.println("s1 == s2: " + (s1 == s2)); // 输出: true
}
}
2️⃣ 懒汉式 (Lazy Initialization)
特点:在首次调用 getInstance() 时才创建实例,节省内存 ,但需要处理线程安全问题。
方式一:线程不安全的懒汉式
这是最基础的懒加载实现,但在多线程环境下是不安全的。
/**
* 懒汉式 - 线程不安全
* 优点:实现了懒加载,节省内存。
* 缺点:在多线程环境下,可能会创建多个实例,导致线程安全问题。
*/
public class LazySingleton_Unsafe {
private LazySingleton_Unsafe() {
System.out.println("LazySingleton_Unsafe 构造方法被调用...");
}
private static LazySingleton_Unsafe instance; // 初始为 null
public static LazySingleton_Unsafe getInstance() {
if (instance == null) { // 1. 检查实例是否已存在
instance = new LazySingleton_Unsafe(); // 2. 如果不存在,则创建
}
return instance;
}
}
// --- 测试线程安全问题 ---
class UnsafeTest {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
LazySingleton_Unsafe singleton = LazySingleton_Unsafe.getInstance();
System.out.println(Thread.currentThread().getName() + " 获取到的实例: " + singleton);
};
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
t1.join(); // 等待 t1 结束
t2.join(); // 等待 t2 结束
// 可能输出两个不同的实例地址,证明线程不安全。
}
}
方式二:线程安全的懒汉式(同步方法)
通过在 getInstance 方法上加 synchronized 关键字来保证线程安全。
/**
* 懒汉式 - 线程安全(同步方法)
* 优点:解决了线程安全问题。
* 缺点:效率低。每次调用 getInstance 都需要获取锁,即使实例已经创建。
*/
public class LazySingleton_SynchronizedMethod {
private LazySingleton_SynchronizedMethod() {
System.out.println("LazySingleton_SynchronizedMethod 构造方法被调用...");
}
private static LazySingleton_SynchronizedMethod instance;
// synchronized 关键字确保同一时间只有一个线程可以执行此方法
public static synchronized LazySingleton_SynchronizedMethod getInstance() {
if (instance == null) {
instance = new LazySingleton_SynchronizedMethod();
}
return instance;
}
}
// --- 测试 ---
class SynchronizedMethodTest {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
LazySingleton_SynchronizedMethod singleton = LazySingleton_SynchronizedMethod.getInstance();
System.out.println(Thread.currentThread().getName() + " 获取到的实例: " + singleton);
};
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
t1.join();
t2.join();
// 会输出同一个实例地址,但性能较差。
}
}
方式三:双重检查锁
这是懒汉式中推荐的高性能实现方式,它结合了懒加载和线程安全,同时避免了同步方法的性能瓶颈。
/**
* 懒汉式 - 双重检查锁 (DCL)
* 优点:线程安全,性能高,实现了懒加载。
* 缺点:代码相对复杂,需要理解 volatile 的作用。
*/
public class LazySingleton_DoubleCheckedLocking {
private LazySingleton_DoubleCheckedLocking() {
System.out.println("LazySingleton_DoubleCheckedLocking 构造方法被调用...");
}
// 1. 使用 volatile 关键字非常重要!
// 它确保 instance 的写入操作对所有线程立即可见,并禁止 JVM 指令重排序。
// 如果没有 volatile,在多线程环境下可能发生指令重排序,
// 导致其他线程拿到一个未完全初始化的对象(例如,对象的内存已分配,但构造函数尚未执行完毕)。
private static volatile LazySingleton_DoubleCheckedLocking instance;
public static LazySingleton_DoubleCheckedLocking getInstance() {
if (instance == null) { // 第一次检查,避免不必要的同步
synchronized (LazySingleton_DoubleCheckedLocking.class) { // 加锁
if (instance == null) { // 第二次检查,确保只创建一个实例
instance = new LazySingleton_DoubleCheckedLocking();
}
} // 释放锁
}
return instance;
}
}
// --- 应用场景示例:日志记录器 ---
class Logger {
private Logger() {
System.out.println("Logger 初始化...");
}
private static volatile Logger instance;
public static Logger getInstance() {
if (instance == null) {
synchronized (Logger.class) {
if (instance == null) {
instance = new Logger();
}
}
}
return instance;
}
public void log(String message) {
System.out.println("[" + System.currentTimeMillis() + "] " + message);
}
}
// --- 测试 ---
class DCLTest {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
LazySingleton_DoubleCheckedLocking singleton = LazySingleton_DoubleCheckedLocking.getInstance();
System.out.println(Thread.currentThread().getName() + " 获取到的实例: " + singleton);
};
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
t1.join();
t2.join();
// 输出同一个实例地址,且性能优于同步方法。
// 使用日志记录器
Logger logger1 = Logger.getInstance();
Logger logger2 = Logger.getInstance();
logger1.log("Hello from Logger1");
logger2.log("Hello from Logger2");
System.out.println("Logger1 == Logger2: " + (logger1 == logger2)); // 输出: true
}
}
方式四:静态内部类
这是推荐的单例实现方式之一,它利用了 JVM 类加载机制来保证线程安全,同时实现了懒加载,且没有性能损耗。
/**
* 静态内部类方式
* 优点:线程安全,利用 JVM 类加载机制保证实例唯一性,实现了懒加载,性能高,无锁。
* 原理:外部类加载时,内部类 SingletonHolder 并不立即加载。
* 只有当调用 getInstance() 时,才会触发 SingletonHolder 的加载和 INSTANCE 的初始化。
* JVM 保证了类加载过程的线程安全。
*/
public class Singleton_StaticInnerClass {
private Singleton_StaticInnerClass() {
System.out.println("Singleton_StaticInnerClass 构造方法被调用...");
}
// 1. 定义一个静态内部类,内部持有单例实例
private static class SingletonHolder {
// 2. 在内部类中创建实例,JVM 保证其只被初始化一次
private static final Singleton_StaticInnerClass INSTANCE = new Singleton_StaticInnerClass();
}
// 3. 提供公共静态方法,返回内部类中创建的实例
public static Singleton_StaticInnerClass getInstance() {
return SingletonHolder.INSTANCE;
}
}
// --- 应用场景示例:缓存管理器 ---
class CacheManager {
private Map<String, Object> cache = new ConcurrentHashMap<>();
private CacheManager() {
System.out.println("CacheManager 初始化...");
}
private static class CacheManagerHolder {
private static final CacheManager INSTANCE = new CacheManager();
}
public static CacheManager getInstance() {
return CacheManagerHolder.INSTANCE;
}
public void put(String key, Object value) {
cache.put(key, value);
}
public Object get(String key) {
return cache.get(key);
}
}
// --- 测试 ---
class StaticInnerClassTest {
public static void main(String[] args) throws InterruptedException {
System.out.println("程序启动,但尚未调用 getInstance...");
// 此时 Singleton_StaticInnerClass 和其内部类 SingletonHolder 都未被加载
Runnable task = () -> {
Singleton_StaticInnerClass singleton = Singleton_StaticInnerClass.getInstance();
System.out.println(Thread.currentThread().getName() + " 获取到的实例: " + singleton);
};
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
t1.join();
t2.join();
// 输出同一个实例地址,且只在第一次调用时创建。
// 使用缓存管理器
CacheManager cache1 = CacheManager.getInstance();
CacheManager cache2 = CacheManager.getInstance();
cache1.put("key1", "value1");
System.out.println("Cached Value: " + cache2.get("key1")); // "value1"
System.out.println("Cache1 == Cache2: " + (cache1 == cache2)); // 输出: true
}
}
方式五:枚举 (Enum)
这是最推荐的单例实现方式。它天然支持线程安全,防止反射和序列化破坏单例。
/**
* 枚举方式
* 优点:实现简单,线程安全,天然防止反射和序列化破坏。
* 原理:枚举实例在类加载时就被创建(饿汉式),并且 JVM 保证了其唯一性。
* 枚举的特殊性质使得它无法被反射创建新实例,也无法被序列化破坏。
*/
public enum Singleton_Enum {
INSTANCE; // 唯一的实例
// 可以定义方法
public void doSomething() {
System.out.println("Singleton_Enum is doing something...");
}
}
// --- 应用场景示例:系统配置 ---
enum SystemConfig {
INSTANCE;
private String version = "1.0.0";
private boolean debugMode = false;
public String getVersion() { return version; }
public boolean isDebugMode() { return debugMode; }
public void setDebugMode(boolean debugMode) { this.debugMode = debugMode; }
}
// --- 测试 ---
class EnumTest {
public static void main(String[] args) {
Singleton_Enum s1 = Singleton_Enum.INSTANCE;
Singleton_Enum s2 = Singleton_Enum.INSTANCE;
System.out.println("s1 == s2: " + (s1 == s2)); // 输出: true
s1.doSomething(); // 调用方法
SystemConfig config = SystemConfig.INSTANCE;
System.out.println("Version: " + config.getVersion());
System.out.println("Debug Mode: " + config.isDebugMode());
}
}
💥 破坏单例模式的两种方式
尽管我们精心设计了单例,但仍有可能被破坏。
1. 序列化与反序列化
当一个单例对象被序列化到文件,再从文件反序列化回来时,会创建一个新的对象,从而破坏单例。
import java.io.*;
// 一个实现了序列化的单例类(例如使用静态内部类方式)
class SingletonWithSerialization implements Serializable {
private SingletonWithSerialization() {
System.out.println("SingletonWithSerialization 构造方法被调用...");
}
private static class SingletonHolder {
private static final SingletonWithSerialization INSTANCE = new SingletonWithSerialization();
}
public static SingletonWithSerialization getInstance() {
return SingletonHolder.INSTANCE;
}
}
// --- 测试破坏 ---
class SerializationBreakTest {
public static void main(String[] args) throws Exception {
SingletonWithSerialization original = SingletonWithSerialization.getInstance();
// 1. 序列化到文件
String filename = "singleton.ser";
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
out.writeObject(original);
}
// 2. 从文件反序列化
SingletonWithSerialization deserialized;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
deserialized = (SingletonWithSerialization) in.readObject();
}
System.out.println("Original == Deserialized: " + (original == deserialized)); // 输出: false
System.out.println("Original: " + original);
System.out.println("Deserialized: " + deserialized);
// 结果证明,反序列化创建了一个新对象,破坏了单例。
}
}
解决方案: 在单例类中添加 readResolve() 方法。
class SingletonWithSerializationFixed implements Serializable {
private SingletonWithSerializationFixed() {
System.out.println("SingletonWithSerializationFixed 构造方法被调用...");
}
private static class SingletonHolder {
private static final SingletonWithSerializationFixed INSTANCE = new SingletonWithSerializationFixed();
}
public static SingletonWithSerializationFixed getInstance() {
return SingletonHolder.INSTANCE;
}
// 3. 添加 readResolve 方法来防止序列化破坏
private Object readResolve() throws ObjectStreamException {
// 当 JVM 从流中读取对象时,会调用此方法,并返回此方法的返回值
// 而不是新建一个对象。这样就保证了单例。
return SingletonHolder.INSTANCE;
}
}
// --- 测试修复 ---
class SerializationFixTest {
public static void main(String[] args) throws Exception {
SingletonWithSerializationFixed original = SingletonWithSerializationFixed.getInstance();
String filename = "singleton_fixed.ser";
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
out.writeObject(original);
}
SingletonWithSerializationFixed deserialized;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
deserialized = (SingletonWithSerializationFixed) in.readObject();
}
System.out.println("Original == Deserialized (Fixed): " + (original == deserialized)); // 输出: true
}
}
2. 反射 (Reflection)
通过反射可以强制访问私有构造方法,从而创建新的实例。
import java.lang.reflect.Constructor;
// 一个线程安全的懒汉式单例
class SingletonWithReflection {
private SingletonWithReflection() {
System.out.println("SingletonWithReflection 构造方法被调用...");
}
private static volatile SingletonWithReflection instance;
public static SingletonWithReflection getInstance() {
if (instance == null) {
synchronized (SingletonWithReflection.class) {
if (instance == null) {
instance = new SingletonWithReflection();
}
}
}
return instance;
}
}
// --- 测试破坏 ---
class ReflectionBreakTest {
public static void main(String[] args) throws Exception {
SingletonWithReflection s1 = SingletonWithReflection.getInstance();
// 1. 获取类的 Class 对象
Class<?> clazz = SingletonWithReflection.class;
// 2. 获取私有构造方法
Constructor<?> constructor = clazz.getDeclaredConstructor();
// 3. 设置可访问(绕过私有访问限制)
constructor.setAccessible(true);
// 4. 通过反射创建新实例
SingletonWithReflection s2 = (SingletonWithReflection) constructor.newInstance();
System.out.println("s1 == s2 (via Reflection): " + (s1 == s2)); // 输出: false
System.out.println("s1: " + s1);
System.out.println("s2: " + s2);
// 结果证明,反射创建了新对象,破坏了单例。
}
}
解决方案: 在私有构造方法中添加防止重复创建的逻辑。
class SingletonWithReflectionFixed {
private static volatile SingletonWithReflectionFixed instance;
private SingletonWithReflectionFixed() {
// 1. 在构造方法中添加检查逻辑
if (instance != null) {
throw new RuntimeException("单例模式被反射破坏!不允许创建多个实例。");
}
System.out.println("SingletonWithReflectionFixed 构造方法被调用...");
}
public static SingletonWithReflectionFixed getInstance() {
if (instance == null) {
synchronized (SingletonWithReflectionFixed.class) {
if (instance == null) {
instance = new SingletonWithReflectionFixed();
}
}
}
return instance;
}
}
// --- 测试修复 ---
class ReflectionFixTest {
public static void main(String[] args) throws Exception {
SingletonWithReflectionFixed s1 = SingletonWithReflectionFixed.getInstance();
Class<?> clazz = SingletonWithReflectionFixed.class;
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
try {
// 尝试通过反射创建第二个实例,会抛出异常
SingletonWithReflectionFixed s2 = (SingletonWithReflectionFixed) constructor.newInstance();
} catch (Exception e) {
System.out.println("捕获异常: " + e.getCause().getMessage()); // 输出异常信息
}
}
}
注意: 枚举方式 enum 天然免疫这两种破坏方式。
🔍 JDK 源码解析:Runtime 类
Runtime 类是 Java 核心库中单例模式的经典应用。
public class Runtime {
// 1. 静态变量在类加载时创建实例,属于饿汉式
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
// 2. 提供公共静态方法获取实例
public static Runtime getRuntime() {
return currentRuntime;
}
// 3. 私有构造方法,防止外部实例化
/** Don't let anyone else instantiate this class */
private Runtime() {}
...
}
// --- 使用示例 ---
class RuntimeDemo {
public static void main(String[] args) throws IOException {
// 获取 Runtime 的唯一实例
Runtime runtime = Runtime.getRuntime();
// 获取 JVM 内存信息
System.out.println("Total Memory: " + runtime.totalMemory());
System.out.println("Max Memory: " + runtime.maxMemory());
// 执行系统命令 (示例:在 Linux/Mac 上用 'ls' 或 Windows 上用 'dir')
Process process = runtime.exec("cmd /c dir"); // Windows 示例
// Process process = runtime.exec("ls -l"); // Linux/Mac 示例
// 读取命令输出
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// 确保进程结束
process.destroy();
}
}
📈 单例模式总结
|-------------|------|--------|----|--------|--------|
| 实现方式 | 线程安全 | 懒加载 | 性能 | 推荐度 | 破坏方式 |
| 饿汉式 (静态变量) | ✅ | ❌ | 高 | ⭐⭐⭐ | 序列化、反射 |
| 饿汉式 (静态代码块) | ✅ | ❌ | 高 | ⭐⭐⭐ | 序列化、反射 |
| 懒汉式 (线程不安全) | ❌ | ✅ | 高 | ⭐ | - |
| 懒汉式 (同步方法) | ✅ | ✅ | 低 | ⭐⭐ | 序列化、反射 |
| 懒汉式 (双重检查锁) | ✅ | ✅ | 高 | ⭐⭐⭐⭐⭐ | 序列化、反射 |
| 静态内部类 | ✅ | ✅ | 高 | ⭐⭐⭐⭐⭐ | 序列化、反射 |
| 枚举 | ✅ | ❌ (饿汉) | 高 | ⭐⭐⭐⭐⭐⭐ | 无 |
🎨 选择建议
- 推荐首选 : 枚举。它是最安全、最简洁的实现,几乎可以应对所有场景。
- 备选方案 : 静态内部类 或 双重检查锁 (DCL)。它们在性能和安全性之间取得了很好的平衡。
- 简单场景 : 如果实例不重,且确定不会被破坏,饿汉式 也是不错的选择。
🚫 优缺点
优点:
- 内存效率: 只有一个实例,减少了内存开销。
- 资源管理: 避免了对共享资源的重复占用。
- 全局访问: 提供了一个全局的访问点。
缺点:
- 扩展性差: 单例类通常没有抽象层,扩展困难。
- 违反单一职责: 单例类既管理自身实例,又执行业务逻辑。
- 单元测试难: 由于其全局状态,难以进行单元测试。