一、单例模式到底要解决什么问题
假设程序中有一个日志管理器:
class Logger {
public:
void log(const char* msg) {}
};
如果所有模块都自己创建:
Logger log1;
Logger log2;
Logger log3;
那么整个程序可能存在多个日志对象。
但有些资源我们希望:
整个程序
↓
只有一个实例
例如:
日志管理器
配置管理器
资源管理器
线程池管理器
这就是:
Singleton
单例模式
它需要满足三个核心条件:
1. 外部不能随便创建对象
2. 外部不能复制出新的对象
3. 提供统一接口获取唯一对象
所以代码设计思路就是:
构造函数 private
↓
禁止外部构造
拷贝构造 delete
↓
禁止复制对象
静态 getInstance()
↓
返回唯一实例
二、面试最推荐手写的单例模式
现代 C++ 中,最适合现场手写的是:
Meyers Singleton
代码可以写得非常短:
class Singleton {
private:
Singleton() {} // 构造函数私有,外部不能创建对象
~Singleton() {} // 析构函数私有,由类内部管理
public:
Singleton(const Singleton&) = delete; // 禁止拷贝构造
Singleton& operator=(const Singleton&) = delete; // 禁止拷贝赋值
static Singleton& getInstance() { static Singleton instance; return instance; } // 返回唯一实例
};
使用:
Singleton& s1 = Singleton::getInstance(); // 获取单例对象
Singleton& s2 = Singleton::getInstance(); // 再次获取的还是同一个对象
这里:
static Singleton instance;
是整个代码最核心的地方。
第一次调用:
Singleton::getInstance();
时:
instance不存在
↓
创建instance
以后再次调用:
instance已经存在
↓
直接返回原对象
所以:
s1
↓
Singleton对象
↑
s2
s1 和 s2 实际上访问的是:
同一个Singleton实例
可以验证:
#include <iostream>
class Singleton {
private:
Singleton() { std::cout << "构造\n"; } // 私有构造
~Singleton() { std::cout << "析构\n"; } // 私有析构
public:
Singleton(const Singleton&) = delete; // 禁止拷贝
Singleton& operator=(const Singleton&) = delete; // 禁止赋值
static Singleton& getInstance() { static Singleton instance; return instance; } // 唯一入口
};
int main() {
Singleton& s1 = Singleton::getInstance(); // 第一次获取
Singleton& s2 = Singleton::getInstance(); // 第二次获取
std::cout << &s1 << '\n'; // 输出地址
std::cout << &s2 << '\n'; // 两个地址相同
}
运行时可以发现:
&s1 == &s2
所以只有一个对象。
三、为什么构造函数要private,拷贝还要delete
先看:
private:
Singleton() {}
如果构造函数是:
public
那么外部就可以:
Singleton s1;
Singleton s2;
这就直接创建两个对象了。
单例模式就失效。
所以:
构造函数private
↓
只能类内部创建对象
但是只把构造函数设为 private 还不够。
例如已经拿到:
Singleton& s1 = Singleton::getInstance();
如果允许拷贝:
Singleton s2 = s1;
又会产生一个新的:
Singleton对象
所以必须禁止:
Singleton(const Singleton&) = delete;
也就是:
禁止拷贝构造
同时还要:
Singleton& operator=(const Singleton&) = delete;
禁止:
拷贝赋值
因此一个单例类基本都会看到:
Singleton(const Singleton&) = delete; // 禁止拷贝构造
Singleton& operator=(const Singleton&) = delete; // 禁止拷贝赋值
如果还想写得更完整,也可以把移动禁止:
Singleton(Singleton&&) = delete; // 禁止移动构造
Singleton& operator=(Singleton&&) = delete; // 禁止移动赋值
完整一点就是:
class Singleton {
private:
Singleton() {} // 私有构造
~Singleton() {} // 私有析构
public:
Singleton(const Singleton&) = delete; // 禁止拷贝构造
Singleton& operator=(const Singleton&) = delete; // 禁止拷贝赋值
Singleton(Singleton&&) = delete; // 禁止移动构造
Singleton& operator=(Singleton&&) = delete; // 禁止移动赋值
static Singleton& getInstance() { static Singleton instance; return instance; } // 获取唯一实例
};
不过面试时间比较紧时,通常写:
Singleton(const Singleton&) = delete; // 禁止拷贝
Singleton& operator=(const Singleton&) = delete; // 禁止赋值
已经能够表达核心思想。
四、为什么这种写法是线程安全的
面试官很可能继续问:
多线程同时第一次调用
getInstance()怎么办?
例如:
Thread A
↓
getInstance()
Thread B
↓
getInstance()
Thread C
↓
getInstance()
如果三个线程同时第一次访问:
static Singleton instance;
会不会创建三个对象?
在:
C++11及以后
函数内部:
局部静态变量
的初始化具有线程安全保证。
也就是说:
多个线程同时进入
↓
static instance只会初始化一次
所以:
static Singleton& getInstance() { static Singleton instance; return instance; }
既实现:
懒加载
又解决:
第一次初始化的线程安全问题
这也是为什么现代 C++ 面试中,通常优先推荐这种写法。
可以直接这样回答:
C++11 开始,函数局部静态变量的初始化由语言保证线程安全,因此多个线程同时第一次调用
getInstance()时,instance也只会构造一次。
注意这里说的是:
单例对象初始化线程安全
并不等于:
Singleton内部所有成员操作自动线程安全
例如:
class Singleton {
private:
int count_ = 0;
public:
void add() { ++count_; }
};
多个线程:
Singleton::getInstance().add();
同时执行:
++count_;
还是可能出现:
数据竞争
所以:
单例创建线程安全
≠
单例内部业务线程安全
如果成员需要多线程访问,仍然可能需要:
std::mutex
std::atomic
等同步方式。
五、懒汉式、饿汉式和面试怎么回答
单例模式还有两个常见概念:
懒汉式
饿汉式
1. 懒汉式
核心:
用的时候才创建
前面的 Meyers Singleton 就属于:
懒加载
static Singleton& getInstance() { static Singleton instance; return instance; } // 第一次调用才创建
如果程序从来不调用:
getInstance();
那么:
instance就不会被创建
这种方式:
按需创建
比较节省资源。
2. 饿汉式
饿汉式:
程序启动阶段就提前创建对象
例如:
class Singleton {
private:
Singleton() {} // 私有构造
static Singleton instance_; // 静态唯一实例
public:
Singleton(const Singleton&) = delete; // 禁止拷贝
Singleton& operator=(const Singleton&) = delete; // 禁止赋值
static Singleton& getInstance() { return instance_; } // 返回唯一对象
};
Singleton Singleton::instance_; // 程序启动阶段构造
可以简单理解:
饿汉式
↓
提前创建
懒汉式
↓
需要时创建
以前还经常见到双重检查锁:
if (instance_ == nullptr) {
std::lock_guard<std::mutex> lock(mutex_);
if (instance_ == nullptr) {
instance_ = new Singleton;
}
}
思路是:
第一次判断
↓
避免对象已经存在时每次都加锁
加锁
↓
防止多线程同时创建
第二次判断
↓
防止两个线程都通过第一次判断
但是如果面试只是让你:
手写线程安全单例
现代 C++ 最简洁的答案还是:
class Singleton {
private:
Singleton() {} // 私有构造
public:
Singleton(const Singleton&) = delete; // 禁止拷贝
Singleton& operator=(const Singleton&) = delete; // 禁止赋值
static Singleton& getInstance() { static Singleton instance; return instance; } // C++11线程安全
};
这个版本非常适合现场手写。
如果面试官问:
单例模式怎么实现?
可以回答:
首先把构造函数设为 private,防止外部直接创建对象;然后删除拷贝构造和拷贝赋值,防止复制出新的实例;最后提供一个 static 的
getInstance()接口,在函数内部定义一个局部 static 对象并返回它。C++11以后局部静态变量初始化具有线程安全保证,所以这种 Meyers Singleton 是比较常见的实现。
如果继续问:
为什么返回引用,不直接返回对象?
因为如果:
static Singleton getInstance();
按值返回,就可能涉及:
复制一个Singleton
与单例设计目标冲突。
所以通常:
static Singleton& getInstance();
返回:
唯一对象的引用
也可以返回指针:
static Singleton* getInstance();
但 Meyers Singleton 使用引用更加直接。
如果继续问:
单例模式有什么缺点?
可以回答:
单例本质上引入了全局状态,使用过多会增加模块耦合,让依赖关系不明显,也会增加单元测试和替换实现的难度,所以只有确实需要全局唯一实例时才适合使用。
最后把手写单例需要记的代码压缩一下:
class Singleton {
private:
Singleton() {} // 1. 私有构造
public:
Singleton(const Singleton&) = delete; // 2. 禁止拷贝
Singleton& operator=(const Singleton&) = delete; // 3. 禁止赋值
static Singleton& getInstance() { static Singleton instance; return instance; } // 4. 唯一实例
};
真正需要记住的其实只有四步:
构造函数private
↓
禁止外部new对象
拷贝构造delete
↓
不能复制
拷贝赋值delete
↓
不能赋值复制
static getInstance()
↓
返回唯一static实例
如果是在笔试中要求:
手写一个线程安全单例模式。
直接写下面这版基本就够:
class Singleton {
private:
Singleton() {} // 构造私有
public:
Singleton(const Singleton&) = delete; // 禁止拷贝
Singleton& operator=(const Singleton&) = delete; // 禁止赋值
static Singleton& getInstance() { static Singleton obj; return obj; } // C++11线程安全单例
};
这也是单例模式最值得优先掌握的面试写法。