目录
[2.1 C++ 实现](#2.1 C++ 实现)
[2.2 Java 实现](#2.2 Java 实现)
[2.3 饿汉式特点分析](#2.3 饿汉式特点分析)
[3.1 C++ 实现](#3.1 C++ 实现)
[3.2 Java 实现](#3.2 Java 实现)
[3.3 懒汉式的线程安全问题](#3.3 懒汉式的线程安全问题)
[3.3.1 C++ 线程安全方案(双检锁)](#3.3.1 C++ 线程安全方案(双检锁))
[3.3.2 Java 线程安全方案(静态内部类 / 枚举)](#3.3.2 Java 线程安全方案(静态内部类 / 枚举))
[3.4 懒汉式特点分析](#3.4 懒汉式特点分析)
[四、饿汉式 vs 懒汉式:核心对比](#四、饿汉式 vs 懒汉式:核心对比)
在软件开发中,单例模式(Singleton Pattern) 是创建型设计模式的核心之一,它保证一个类仅有一个实例,并提供一个全局访问点,避免重复创建对象带来的资源浪费,同时保证状态一致性。
本文将结合 C++ 和 Java 代码,深入解析单例模式的两种经典实现:饿汉式(Eager Initialization) 与 懒汉式(Lazy Initialization),帮你彻底理解其原理、差异与工程实践。
一、单例模式核心思想
单例模式的核心目标只有两个:
- 控制实例数量 :确保一个类在运行期间最多只存在一个实例。
- 全局访问:提供一个全局入口,让任何地方都能获取到这个唯一实例。
为了实现这两个目标,单例模式必须满足三个约束:
- 构造函数私有化:禁止外部通过
new直接创建实例。 - 禁用拷贝构造与赋值运算符:防止实例被复制,破坏唯一性。
- 提供静态访问方法:作为获取实例的唯一全局入口。
二、饿汉式单例:类加载即初始化
饿汉式的核心思想是 **"急切初始化"**------ 在类加载阶段就创建好唯一实例,不管后续是否会使用它,就像 "饿汉" 一样迫不及待。
2.1 C++ 实现
cpp
#pragma once
#include <iostream>
// 饿汉模式
class Bully {
public:
// 禁用拷贝构造和赋值运算符
Bully(const Bully&) = delete;
Bully& operator=(const Bully&) = delete;
// 全局访问点:返回唯一实例的引用
static Bully& get() {
return person;
}
private:
// 私有构造函数:禁止外部实例化
Bully() {}
// 静态成员变量:类加载时初始化
static Bully person;
};
// 类外初始化静态成员(必须),自动调用构造函数
Bully Bully::person;
2.2 Java 实现
java
// 恶汉模式(饿汉式)
class SingletonBully {
// 类加载时即创建实例,private 保证外部无法直接访问
private static SingletonBully instance = new SingletonBully();
// 私有构造函数
private SingletonBully() {}
// 全局访问点
public static SingletonBully getInstance() {
return instance;
}
}
2.3 饿汉式特点分析
| 优点 | 缺点 |
|---|---|
| ✅ 实现简单,代码清晰 | ❌ 类加载时即初始化,可能造成内存浪费(若实例从未使用) |
| ✅ 天生线程安全(类加载由 JVM/C++ 运行时保证) | ❌ 无法延迟加载,灵活性较低 |
| ✅ 无锁,性能极高 | ❌ 不适合实例创建成本高、使用频率低的场景 |
三、懒汉式单例:延迟初始化,按需创建
懒汉式的核心思想是 **"延迟初始化"**------ 只有在第一次获取实例时才创建对象,就像 "懒汉" 一样,不到万不得已不干活。
3.1 C++ 实现
cpp
#pragma once
#include <iostream>
// 懒汉模式
class Fed {
public:
// 禁用拷贝构造和赋值运算符
Fed(const Fed&) = delete;
Fed& operator=(const Fed&) = delete;
// 全局访问点:第一次调用时创建实例
static Fed& get() {
if (instance == nullptr) {
instance = new Fed();
}
return *instance;
}
private:
// 私有构造函数
Fed() {}
// 静态指针:初始为 nullptr,延迟初始化
static Fed* instance;
};
// 类外初始化静态指针
//static Fed* instance = nullptr;
Fed* Fed::instance = nullptr;
⚠️ 注意:
static Fed* instance = nullptr;多写了一个static,正确写法是Fed* Fed::instance = nullptr;。
3.2 Java 实现
java
// 懒汉模式
class SingletonFed {
// 静态变量初始为 null,延迟初始化
private static SingletonFed instance;
// 私有构造函数
private SingletonFed() {}
// 全局访问点:第一次调用时创建实例
public static SingletonFed getInstance() {
if (instance == null) {
instance = new SingletonFed();
}
return instance;
}
}
3.3 懒汉式的线程安全问题
基础版懒汉式在单线程环境 下是安全的,但在多线程环境下会出现问题:
- 多个线程同时进入
if (instance == null)判断,可能导致多个实例被创建,破坏单例约束。
3.3.1 C++ 线程安全方案(双检锁)
cpp
#include <mutex>
class Fed {
public:
static Fed& get() {
// 第一次检查:无锁,提高性能
if (instance == nullptr) {
// 加锁:保证只有一个线程能进入创建逻辑
std::lock_guard<std::mutex> lock(mtx);
// 第二次检查:防止多线程重复创建
if (instance == nullptr) {
instance = new Fed();
}
}
return *instance;
}
private:
static Fed* instance;
static std::mutex mtx; // 互斥锁
};
Fed* Fed::instance = nullptr;
std::mutex Fed::mtx;
3.3.2 Java 线程安全方案(静态内部类 / 枚举)
-
静态内部类(推荐) :利用类加载机制实现延迟初始化与线程安全。
javaclass SingletonFed { private SingletonFed() {} // 静态内部类:只有在被调用时才会加载,延迟初始化 private static class Holder { private static final SingletonFed INSTANCE = new SingletonFed(); } public static SingletonFed getInstance() { return Holder.INSTANCE; } } -
枚举(最安全) :Java 枚举天然保证单例,且防止反射与序列化破坏单例。
javaenum SingletonFed { INSTANCE; public void doSomething() { // 业务逻辑 } }
3.4 懒汉式特点分析
| 优点 | 缺点 |
|---|---|
| ✅ 延迟加载:只有在第一次使用时才创建实例,节省内存 | ❌ 基础版线程不安全,需要额外处理(加锁 / 静态内部类) |
| ✅ 适合实例创建成本高、使用频率低的场景 | ❌ 加锁版本会带来一定性能开销 |
| ✅ 灵活性高,可根据业务场景调整初始化时机 | ❌ 实现复杂度略高于饿汉式 |
四、饿汉式 vs 懒汉式:核心对比
| 对比维度 | 饿汉式 | 懒汉式 |
|---|---|---|
| 实例创建时机 | 类加载阶段 | 第一次获取实例时 |
| 线程安全 | 天生安全 | 基础版不安全,需额外处理 |
| 内存占用 | 类加载时即占用,可能浪费 | 延迟占用,按需分配 |
| 性能 | 无锁,性能极高 | 加锁版本性能略低 |
| 实现复杂度 | 简单 | 略复杂(需处理线程安全) |
| 适用场景 | 实例小、使用频繁 | 实例大、使用频率低 |
五、单例模式的最佳实践
-
优先考虑饿汉式:如果实例创建成本低、使用频繁,饿汉式是最简单高效的选择。
-
延迟加载用懒汉式:如果实例创建成本高(如连接池、大对象),且可能不会被使用,选择懒汉式。
-
Java 推荐枚举 / 静态内部类:枚举是最安全的单例实现,静态内部类是延迟初始化的优雅方案。
-
C++ 推荐 C++11 静态局部变量 :C++11 后,静态局部变量的初始化是线程安全的,可极简实现懒汉式:
cppclass Fed { public: static Fed& get() { static Fed instance; // 线程安全的延迟初始化 return instance; } private: Fed() {} }; -
警惕单例的滥用:单例模式会引入全局状态,增加代码耦合度,不利于单元测试,需谨慎使用。
六、总结
单例模式是软件开发中最常用的设计模式之一,饿汉式 和懒汉式是其两种核心实现:
- 饿汉式:以空间换时间,简单高效,线程安全。
- 懒汉式:以时间换空间,延迟加载,需处理线程安全。
在实际开发中,应根据业务场景选择合适的实现方式,同时注意单例模式的局限性,避免过度使用。