【设计模式】单例模式
单例模式
- 单例模式的概念与定义
- 单例模式的分类
- 线程安全问题
- 案例程序—创建一个单例任务队列
单例模式的概念与定义
单例模式在创建型模式中用的非常多
因为在一个项目中,全局范围内,某个类的实例有且仅有一个,通过这个唯一实例向其他模块提供数据的全局访问,这种模式就叫单例模式。单例模式的典型应用就是任务队列、全局信号总线管理器、堆区管理器等。
如果使用单例模式,首先要保证这个类的实例有且仅有一个,因此,就必须采取一系列的防护措施。对于类来说以上描述同样适用。涉及一个类多对象操作的函数有以下几个:
- 构造函数: 创建一个新的对象
- 拷贝构造函数: 根据已有对象拷贝出一个新的对象
- 拷贝赋值操作符重载函数: 两个对象之间的赋值
为了把一个类可以实例化多个对象的路堵死,可以做如下处理:
- 构造函数私有化,在类内部只调用一次,这个是可控的
- 拷贝构造函数私有化或者禁用(使用 =
delete
) - 拷贝赋值操作符重载函数私有化或者禁用
应该私有化或者删除后,无法在类外创建实例了,由于使用者在类外部不能使用构造函数,只能通过类名来得到,那内部的为类的内部静态对象。
-
在类中定义静态成员,即属于类的静态实例对象
private:static Singleton* m_obj; // 单例对象
-
由于私有的静态成员变量,只能通过公共的静态方法获得,给这个单例类提供一个静态函数用于得到这个静态的单例对象:
public:static Singleton* getInstance(){return m_obj;}
-
使用的时候,静态变量必须在类外部对其进行初始化
Singleton* Singleton::m_obj = new Singleton;
-
main函数创建实例对象
int main() {// 创建对象Singleton* obj1 = Singleton::getInstance();obj1->printf();return 0; }
其单例模式UML图与案例程序如下:

class Singleton
{
public:static Singleton* getInstance(){return m_obj;}void printf(){cout << "hello world" << endl;}// 拷贝赋值操作符重载函数函数,也可以使用私有化Singleton& operator=(const Singleton& obj) = delete;
protected:private:// 构造函数私有化Singleton() = default; // 拷贝构造函数私有化Singleton(const Singleton& obj) = default;static Singleton* m_obj; // 单例对象
};
// 初始化静态成员变量
Singleton* Singleton::m_obj = new Singleton;
单例模式的分类
单例模式又分为饿汉式模式与懒汉式模式: 根据对类的静态成员变量的初始化是否为空进行分类
-
饿汉模式就是在类加载的时候立刻进行实例化,这样就得到了一个唯一的可用对象。关于这个饿汉模式的类的定义如下:
// 饿汉模式 -- 定义类的时候创建单例对象 class Singleton { public:// = delete 代表函数禁用, 也可以将其访问权限设置为私有Singleton(const Singleton& obj) = delete;Singleton& operator=(const Singleton& obj) = delete;static Singleton* getInstance(){return m_obj;}private:Singleton() = default; // 构造函数私有化,饿汉式构造函数不能删除,必须私有并默认static Singleton* m_obj; // 单例对象 }; // 初始化静态成员变量 Singleton* Singleton::m_obj = new Singleton;// 定义一个单例模式的实例对象 int main() {// 创建对象Singleton* obj1 = Singleton::getInstance();return 0; }
-
懒汉模式在类加载的时候不去创建这个唯一的实例,而是在需要使用的时候再进行实例化,相比饿汉式,节省内存空间
// 懒汉模式 -- 什么时候使用这个单例对象, 在使用的时候再去创建对应的实例 // class Singleton { public:Singleton(const Singleton& obj) = delete;Singleton& operator=(const Singleton& obj) = delete;static Singleton* getInstance(){if(m_obj == nullptr){m_obj = new Singleton();}return m_obj;}private:Singleton() = default;static Singleton* m_obj; // 单例对象 }; // 初始化静态成员变量 Singleton* Singleton::m_obj = nullptr;
线程安全问题
- 对于饿汉模式是没有线程安全问题的,在这种模式下多线程访问单例对象(getInstance)的时候,这个对象已经被创建出来了,只做读取
- 对于懒汉模式的线程安全问题,最常用的解决方案就是使用互斥锁。可以将创建单例对象的代码使用互斥锁锁住,处理代码如下:
class Singleton
{
public:Singleton(const Singleton& obj) = delete;Singleton& operator=(const Singleton& obj) = delete;static Singleton* getInstance(){m_mutex.lock(); // 加锁if(m_obj == nullptr){m_obj = new Singleton();}m_mutex.unlock();return m_obj;}void printf(){cout << "hello world" << endl;}protected:private:Singleton() = default;static Singleton* m_obj; static mutex m_mutex; // 定义一把互斥锁
};
// 初始化静态成员变量
Singleton* Singleton::m_obj = nullptr;
mutex Singleton::m_mutex;
分析以上代码,发现 getInstance
方法因为加了互斥锁,对于每个线程而言,降低了效率,多个线程无法同时执行,会被阻塞。
改进:双重检查锁定
static Singleton* getInstance()
{if(m_obj == nullptr){m_mutex.lock(); // 加锁if(m_obj == nullptr){m_obj = new Singleton();}m_mutex.unlock();}return m_obj;
}
但是实际上 m_taskQ = new TaskQueue;
在执行过程中对应的机器指令可能会被重新排序。正常过程如下:
-
第一步:分配内存用于保存 TaskQueue 对象。
-
第二步:在分配的内存中构造一个 TaskQueue 对象(初始化内存)
-
第三步:使用 m_taskQ 指针指向分配的内存。
但是被重新排序以后执行顺序可能会变成这样:
-
第一步:分配内存用于保存 TaskQueue 对象。
-
第二步:使用 m_taskQ 指针指向分配的内存。
-
第三步:在分配的内存中构造一个 TaskQueue 对象(初始化内存)。
这样重排序并不影响单线程的执行结果,但是在多线程中就会出问题。如果线程A按照第二种顺序执行机器指令,执行完前两步之后失去CPU时间片被挂起了,此时线程B在第3行处进行指针判断的时候m_taskQ
指针是不为空的,但这个指针指向的内存却没有被初始化,最后线程 B 使用了一个没有被初始化的队列对象就出问题了(概率问题)
在C++11中引入了原子变量atomic(在底层控制了机器指令的执行顺序),通过原子变量可以实现一种更安全的懒汉模式的单例,代码如下:
class Singleton
{
public:Singleton(const Singleton& obj) = delete;Singleton& operator=(const Singleton& obj) = delete;// 使用原子变量static Singleton* getInstance(){Singleton* obj = m_obj.load(); // 读取原子变量的值 if(obj == nullptr){m_mutex.lock(); // 加锁obj = m_obj.load(); // 读取原子变量的值if(obj == nullptr){obj = new Singleton();m_obj.store(obj); // 保存到原子变量}m_mutex.unlock();}return obj;}private:Singleton() = default;static atomic<Singleton*> m_obj; // 单例对象,原子变量管理static mutex m_mutex;
};// 初始化静态成员变量
atomic<Singleton*> Singleton::m_obj;
mutex Singleton::m_mutex;
上面代码中使用原子变量 atomic
的 store()
方法来存储单例对象,使用 load()
方法来加载单例对象。在原子变量中这两个函数在处理指令的时候默认的原子顺序是memory_order_seq_cst
(顺序原子操作 - sequentially consistent
),使用顺序约束原子操作库,整个函数执行都将保证顺序执行,并且不会出现数据竞态(data races),不足之处就是使用这种方法实现的懒汉模式的单例执行效率更低一些。
方法2: 使用静态的局部对象解决线程安全问题 ---->>>> 编译器支持C++11
class Singleton
{
public:Singleton(const Singleton& obj) = delete;Singleton& operator=(const Singleton& obj) = delete;// 使用原子变量static Singleton* getInstance(){static Singleton obj;return &obj;}private:Singleton() = default;
};
定义了一个静态局部队列对象,并且将这个对象作为了唯一的单例实例。使用这种方式之所以是线程安全的,是因为在C++11标准中有如下规定,并且这个操作是在编译时由编译器保证的:如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化。
总结: 懒汉模式的缺点是在创建实例对象的时候有安全问题,但这样可以减少内存的浪费(如果用不到就不去申请内存了)。饿汉模式则相反,在我们不需要这个实例对象的时候,它已经被创建出来,占用了一块内存。对于现在的计算机而言,内存容量都是足够大的,这个缺陷可以被无视。
案例程序—创建一个单例任务队列
#include <iostream>
#include <queue>
#include <mutex>
#include <thread>using namespace std;// 饿汉模式
class TaskQueue
{
public:TaskQueue(const TaskQueue& queue) = delete; // 删除拷贝构造函数TaskQueue& operator=(const TaskQueue& queue) = delete; // 删除拷贝赋值操作符重载函数// 公有的获取实例对象static TaskQueue* getInstance() {return taskQueue;}// 添加任务void addTask(int task){lock_guard<mutex> lock(m_mutex);m_que.push(task);}// 删除任务bool popTask() {lock_guard<mutex> lock(m_mutex);if(m_que.empty())return false;m_que.pop();return true;}// 获取任务int getTask() {lock_guard<mutex> locker(m_mutex);if(m_que.empty())return -1;int task = m_que.front();return task;}// 判断任务队列是否为空bool isEmpty() {lock_guard<mutex> lock(m_mutex);bool flag = m_que.empty();return flag;}private:TaskQueue() = default; // 构造函数私有化static TaskQueue* taskQueue; // 单例对象// 定义任务队列queue<int> m_que;mutex m_mutex;
};
// 类外部初始化类的静态成员变量
TaskQueue* TaskQueue::taskQueue = new TaskQueue();void func1(TaskQueue* taskQueue)
{// 生产者for (int i = 0; i < 20; i++) {taskQueue->addTask(i + 100);cout << "+++ push data: " << i + 100 << ", threadID: " << this_thread::get_id() << endl;this_thread::sleep_for(chrono::milliseconds(100)); // 休眠一定的时间长度}}void func2(TaskQueue* taskQueue)
{// 消费者this_thread::sleep_for(chrono::milliseconds(500)); while (!taskQueue->isEmpty()){int task = taskQueue->getTask();taskQueue->popTask();cout << "--- get data: " << task << ", threadID: " << this_thread::get_id() << endl;this_thread::sleep_for(chrono::milliseconds(500)); }
}int main()
{TaskQueue* taskQueue = TaskQueue::getInstance();thread thread1(func1, taskQueue); // 生产者线程thread thread2(func2, taskQueue); // 消费者线程thread1.join();thread2.join();return 0;
}