1. 什么是单例模式
单例模式(Singleton Pattern)是一种创建型设计模式,它保证一个类在整个应用程序生命周期内只有一个实例,并提供一个全局访问点来获取这个唯一实例。
单例模式的核心思想可以概括为三点:
- 私有构造方法 :将构造函数设为私有,禁止外部通过
new直接创建对象。 - 静态持有实例:在类内部维护一个静态成员变量,保存唯一实例。
- 全局访问入口 :提供一个公开的静态方法(通常命名为
getInstance()),供外部获取该实例。
下面是一个最基础的单例模式实现示例(C++):
cpp
class Singleton {
private:
// 静态持有唯一实例
static Singleton* instance;
// 私有构造方法,禁止外部 new
Singleton() {}
public:
// 全局访问入口
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};
// 静态成员变量定义
Singleton* Singleton::instance = nullptr;
上面的写法称为「懒汉式」,即第一次调用 getInstance() 时才创建实例。它虽然简单,但在多线程环境下存在线程安全问题,后面会详细讨论。
2. 单例模式能解决哪些问题
单例模式主要解决以下四类问题:
2.1 保证全局唯一性
某些对象在业务上必须全局唯一,例如配置管理器、日志记录器、线程池、数据库连接池等。如果允许创建多个实例,会导致状态不一致、资源重复占用甚至数据错乱。
单例模式通过私有构造函数和静态实例,从语言层面强制保证「只有一个实例」,避免开发者误用 new 创建出多个对象。
2.2 节省系统资源
创建对象往往需要消耗内存、CPU 或外部连接资源。以数据库连接池为例,如果每次使用都新建连接,会频繁建立和销毁 TCP 连接,性能开销巨大。
单例模式让这类重量级对象只创建一次,后续所有调用方复用同一个实例,从而显著降低资源消耗。
2.3 提供全局访问点
单例模式通过静态方法 getInstance() 提供统一的全局访问入口,任何模块都可以方便地获取该实例,无需层层传递对象引用。
这在工具类、配置中心、缓存管理等场景中非常实用,简化了代码结构,也降低了模块间的耦合。
2.4 保证数据一致性
当多个模块需要共享同一份状态数据时(例如全局配置、用户会话、计数器),单例模式确保所有模块读写的是同一份数据,避免因多实例导致的数据不一致问题。
3. 单例模式的常见实现方式
根据线程安全和延迟加载的需求,单例模式有多种实现方式,下面逐一介绍。
3.1 饿汉式(线程安全)
饿汉式在类加载时就创建实例,天然线程安全,但可能造成资源浪费(即使从未使用也会创建)。
cpp
class Singleton {
private:
// 类加载时即创建实例(静态初始化)
static Singleton* instance;
// 私有构造方法
Singleton() {}
public:
static Singleton* getInstance() {
return instance;
}
};
// 静态成员变量定义(程序启动时创建)
Singleton* Singleton::instance = new Singleton();
3.2 懒汉式 + 同步方法(线程安全但性能差)
在懒汉式基础上给 getInstance() 加 synchronized,保证线程安全,但每次调用都会加锁,性能较差。
cpp
class Singleton {
private:
static Singleton* instance;
static std::mutex mutex_;
Singleton() {}
public:
static Singleton* getInstance() {
std::lock_guard<std::mutex> lock(mutex_);
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex_;
3.3 双重检查锁(推荐)
双重检查锁(Double-Checked Locking)只在实例为空时才加锁,兼顾线程安全和性能,是实际开发中最常用的写法。
cpp
class Singleton {
private:
// 原子指针防止指令重排序
static std::atomic<Singleton*> instance;
static std::mutex mutex_;
Singleton() {}
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton();
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mutex_;
3.4 静态内部类(推荐)
静态内部类方式利用 JVM 的类加载机制,既实现延迟加载,又天然线程安全,代码简洁优雅。
cpp
class Singleton {
private:
Singleton() {}
public:
// 利用函数局部静态变量实现延迟加载和线程安全
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
};
3.5 枚举方式(最简洁)
枚举方式由 JVM 保证线程安全和唯一性,还能防止反射和序列化破坏单例,是《Effective Java》推荐的写法。
java
public enum Singleton {
INSTANCE;
public void doSomething() {
// 业务方法
}
}
4. 单例模式的适用场景与注意事项
4.1 适用场景
- 配置管理:全局配置信息只需加载一次,供所有模块读取。
- 日志记录器:日志组件全局唯一,避免日志文件写入冲突。
- 数据库连接池 / 线程池:重量级资源复用,避免频繁创建销毁。
- 缓存管理:全局缓存对象统一管理,保证数据一致性。
- 工具类:无状态工具类(如日期格式化、ID 生成器)使用单例避免重复创建。
4.2 注意事项
- 线程安全:多线程环境下必须选择线程安全的实现方式(饿汉式、双重检查锁、静态内部类、枚举)。
- 反射攻击:反射可以调用私有构造方法破坏单例,可在构造函数中加防反射校验。
- 序列化破坏 :实现
Serializable时需重写readResolve()方法,防止反序列化创建新实例。 - 滥用风险:单例模式本质上是全局状态,过度使用会增加模块间隐式耦合,降低代码可测试性,应谨慎使用。
5. 总结
单例模式是一种基础且实用的创建型设计模式,它通过私有构造函数和静态实例,保证类在全局只有一个实例,并提供统一的访问入口。
它主要解决四类问题:保证全局唯一性 、节省系统资源 、提供全局访问点 、保证数据一致性。在实现时,应根据线程安全和延迟加载需求选择合适的写法,推荐使用双重检查锁、静态内部类或枚举方式。
最后要提醒的是,单例模式虽好,但不要滥用。它本质上是全局状态,过度使用会带来耦合和测试困难,只有在确实需要全局唯一实例时才值得使用。