在一个项目中,全局范围内,某个类的实例有且仅有一个,通过这个唯一实例向其他模块提供数据的全局访问,这种模式就叫单例模式。
类中多对象的操作函数有如下几个:
构造函数
: 能够创建出一个新对象;拷贝构造函数
:能够根据一个已经存在的对象拷贝出一个新对象;赋值操作符重载函数
:用一个对象给另一个对象赋值;为了使得类全局只有一个实例,我们需要对这些函数做一些处理:
private
。private
或者 delete
);单例模式的代码模板:
// 定义一个单例模式的类
class Singleton
{
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
Singleton(const Singleton& rhs) = delete;
Singleton& operator=(const Singleton& rhs) = delete;
static Singleton* getInstance();
private:
Singleton() = default;
static Singleton* m_obj;
};
单例模式可以分为 :懒汉式 和 饿汉式。
饿汉模式就是在类加载的时候立刻进行实例化,这样就得到了一个唯一的可用对象。
定义:
// 饿汉模式 在调用 get_instance 之前 实例就已经存在了
// 多线程环境下 , 饿汉模式是线程 安全的
class TaskQueue {
public:
TaskQueue(const TaskQueue& rhs) = delete;
TaskQueue& operator = (const TaskQueue& rhs) = delete;
static TaskQueue* get_instance() {
return m_task_queue;
}
void print() {
cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;
}
private:
TaskQueue() = default;
static TaskQueue* m_task_queue;
};
TaskQueue* TaskQueue::m_task_queue = new TaskQueue;
int main()
{
TaskQueue* task_queue = TaskQueue::getInstance();
task_queue->print();
}
需要注意的是:
TaskQueue
类的时候,这个静态的单例对象 m_task_queue
就已经被创建出来了,当调用 TaskQueue::get_instance()
的时候,对象就已经被实例化了;懒汉式是在类加载的时候不去创建这个唯一的实例,而是在需要使用的时候再进行实例化。
定义:
// 懒汉模式 在调用 get_instance 之前 实例存在 , 第一次调用 get_instance 才会实例化对象
// 多线程环境下, 饿汉模式是线程 不安全的
class TaskQueue {
public:
TaskQueue(const TaskQueue& rhs) = delete;
TaskQueue& operator = (const TaskQueue& rhs) = delete;
static TaskQueue* get_instance() {
if (m_task_queue == nullptr) {
//在第一次调用 get_instance() 的时候再初始化
m_task_queue = new TaskQueue;
}
return m_task_queue;
}
void print() {
cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;
}
private:
TaskQueue() = default;
static TaskQueue* m_task_queue;
};
TaskQueue* TaskQueue::m_task_queue = nullptr;
int main()
{
TaskQueue* task_queue = TaskQueue::getInstance();
task_queue->print();
}
上述代码在单线程环境下是没问题的。但是在多线程环境下,就会出问题,假设多个线程同时调用 get_instance()
函数,并且此时 m_task_queue = nullptr
,那么就可能创建出多个实例,这就不符合单例模式的定义。
我们可以使用互斥锁 mutex
将创建实例的代码锁住,第一次只有一个线程进来创建对象。
代码:
// 用 双重检测锁定 解决懒汉式多线程环境下线程不安全的问题
class TaskQueue {
public:
TaskQueue(const TaskQueue& rhs) = delete;
TaskQueue& operator = (const TaskQueue& rhs) = delete;
static TaskQueue* get_instance() {
m_mutex.lock(); //加锁
if (m_task_queue== nullptr)
{
m_task_queue= new TaskQueue;
}
m_mutex.unlock(); //解锁
return m_task_queue;
}
void print() {
cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;
}
private:
TaskQueue() = default;
static TaskQueue* m_task_queue;
static mutex m_mutex;
};
TaskQueue* TaskQueue::m_task_queue = nullptr;
mutex TaskQueue::m_mutex;
上面代码虽然解决了问题,但是 get_instance()
中的锁住的代码段,每次就只有一个线程来访问,这样效率就非常低。
双重检测锁定的思路是:在加锁和解锁代码块 之外再加一个 if
判断。这样的话,在第一次调用 get_instance()
的线程仍然会阻塞;第二次调用 get_instance()
的线程,此时 m_task_queue
已经被实例化了,也就是不为 nullptr
了,那么第二次的线程在来到一个 if
判断的时候,就直接退出了,不需要再加锁解锁,这样效率就提升了。
代码:
// 用 双重检测锁定 解决懒汉式多线程环境下线程不安全的问题
class TaskQueue {
public:
TaskQueue(const TaskQueue& rhs) = delete;
TaskQueue& operator = (const TaskQueue& rhs) = delete;
static TaskQueue* get_instance() {
//外面再加一层判断
if (m_task_queue == nullptr) {
m_mutex.lock();
if (m_task_queue == nullptr) {
m_task_queue = new TaskQueue;
}
m_mutex.unlock();
}
return m_task_queue;
}
void print() {
cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;
}
private:
TaskQueue() = default;
static TaskQueue* m_task_queue;
static mutex m_mutex;
};
TaskQueue* TaskQueue::m_task_queue = nullptr;
mutex TaskQueue::m_mutex;
实际上 双重检测锁定 的代码还是有问题的。
假设此时有两个线程 A
和 B
,线程 A
刚好要调用 m_task_queue = new TaskQueue;
这一句代码(假设此时 m_task_queue == nullptr
);而线程 B
刚好来到第一个 if
判断。
static TaskQueue* get_instance() {
//线程B 马上进入下面这个 if 判断
if (m_task_queue == nullptr) {
m_mutex.lock();
if (m_task_queue == nullptr) {
//线程A 马上调用下面这一句代码
m_task_queue = new TaskQueue;
}
m_mutex.unlock();
}
return m_task_queue;
}
对于 m_task_queue = new TaskQueue;
创建对象的这一句代码,在底层实际上时会被分成三个步骤:
TaskQueue
对象;TaskQueue
对象(初始化内存);m_task_queue
指向分配的内存;由于编译器底层对我们的代码进行优化,就会将这些指令进行重排序,也就是打乱了它本来的步骤。
比如说将上述的步骤重排序之后,变成下面的:
TaskQueue
对象;m_task_queue
指向分配的内存;TaskQueue
对象(初始化内存);即 第二步 和 第三步 颠倒了顺序。
指令重排序在单线程下没有问题,在多线程下就有可能出现问题。
假设线程 A
此时刚好把前两步执行完了,m_task_queue
此时已经指向一块内存了,不过对这块内存进行操作是非法操作,因为创建对象还没有完成;线程 B
此时正好,进入第一个 if
判断,此时 m_task_queue
不为 nullptr
,就直接退出,返回了没有构造完全的对象 m_task_queue
。
如果线程 B
对这个对象进行操作,就会出问题。
C++ 11 引入了 原子变量 atomic
可以解决 双重检测锁定 的问题。
代码:
// 用 原子变量 解决双重检测 的问题
class TaskQueue {
public:
TaskQueue(const TaskQueue& rhs) = delete;
TaskQueue& operator = (const TaskQueue& rhs) = delete;
static TaskQueue* get_instance() {
TaskQueue* task_queue = m_task_queue.load();
if (task_queue == nullptr) {
m_mutex.lock();
task_queue = m_task_queue.load();
if (task_queue == nullptr) {
task_queue = new TaskQueue;
m_task_queue.store(task_queue);
}
m_mutex.unlock();
}
return task_queue;
}
void print() {
cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;
}
private:
TaskQueue() = default;
//static TaskQueue* m_task_queue;
static atomic<TaskQueue*> m_task_queue;
static mutex m_mutex;
};
//TaskQueue* TaskQueue::m_task_queue = nullptr;
atomic<TaskQueue*> TaskQueue::m_task_queue;
mutex TaskQueue::m_mutex;
上面代码中使用原子变量 atomic
的 store()
函数来存储单例对象,使用 load()
函数来加载单例对象。
在原子变量中这两个函数在处理指令的时候默认的原子顺序是 memory_order_seq_cst
(即顺序原子操作 - sequentially consistent),这样也就避免了之前的指令重排的问题,使用顺序约束原子操作库,整个函数执行都将保证顺序执行,并且不会出现数据竞态(data races),缺点就是使用这种方法实现的懒汉模式的单例执行效率更低一些。
在 C++ 11 直接使用 静态局部变量 在多线程环境下是不会出现问题的。
代码:
class TaskQueue
{
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
TaskQueue(const TaskQueue& rhs) = delete;
TaskQueue& operator=(const TaskQueue& rhs) = delete;
static TaskQueue* getInstance()
{
static TaskQueue task_queue;
return &task_queue;
}
void print()
{
cout << "我是单例对象 TaskQueue 的一个成员函数..." << endl;
}
private:
TaskQueue() = default;
};
int main()
{
TaskQueue* queue = TaskQueue::getInstance();
queue->print();
return 0;
}
之所以上面代码是线程安全的 ,是因为 C++ 11 规定了,并且这个操作是在编译时由编译器保证的:
如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化。
实现一个 任务队列。生产者线程生产任务加入任务队列;消费者线程取出任务队列的任务执行。
类成员:
queue
;mutex
),在多线程访问的情况下,用于保护共享数据;成员函数:
为了简单起见,我们用一个 int
数,表示一个任务。
代码:
#if 1
// 用局部静态变量饿汉式单例 实现任务队列
class TaskQueue {
public:
TaskQueue(const TaskQueue& rhs) = delete;
TaskQueue& operator = (const TaskQueue& rhs) = delete;
static TaskQueue* get_instance() {
static TaskQueue task_queue;
return &task_queue;
}
//判断任务队列是否为空
bool is_empty() {
lock_guard<mutex> locker(m_mutex);
return q.empty();
}
//删除任务
bool delete_task() {
lock_guard<mutex> locker(m_mutex);
if (q.empty()) return false;
q.pop();
return true;
}
//取出任务 (不删除任务)
int take_task() {
lock_guard<mutex> locker(m_mutex);
if (q.empty()) return -1;
return q.front();
}
//添加任务
void add_task(int task) {
lock_guard<mutex> locker(m_mutex);
q.push(task);
}
private:
TaskQueue() = default;
queue<int> q;
mutex m_mutex;
};
#endif
int main() {
TaskQueue* task_queue = TaskQueue::get_instance();
thread t1([=]() {
//生产者 t1 给任务队列添加10个任务
for (int i = 0; i < 10; i++) {
int task = i + 100;
task_queue->add_task(task);
cout << "producer thread produce a task : " << task << " , thread id is " << this_thread::get_id() << endl;
this_thread::sleep_for(chrono::milliseconds(500));
}
});
thread t2([=](){
//让生产者线程先执行 保证先有任务
this_thread::sleep_for(chrono::milliseconds(500));
while (!task_queue->is_empty()) {
int task = task_queue->take_task();
task_queue->delete_task();
cout << "consumer thread consume a task : " << task << " , thread id is " << this_thread::get_id() << endl;
this_thread::sleep_for(chrono::milliseconds(1000));
}
});
t1.join();
t2.join();
return 0;
}
本篇博客是对于 :单例模式 的整理。