什么是单例模式?
在一个项目中的全局范围内, 一个类有且仅有一个实例对象。这个唯一的实例对象给其他模块提供数据的全局访问。这样的模式就叫单例模式。
单例模式的典型例子就是任务队列。
那么如何去实现这样的一个单例模式的类?
首先, 考虑单例模式的要求为有且仅有一个实例对象。那么就先从构造函数入手。类的构造函数主要有:构造函数、拷贝构造函数、赋值运算符重载构造函数。
- 对于构造函数,将构造函数的访问权限设为
private
, 这样就禁止了构造函数在类的外部被调用。而在类的内部保证只调用构造函数一次,这样就创建了类的唯一对象。又因为单例对象需要提供数据的全局访问,所以将这个唯一对象声明为 静态变量,静态变量的生命周期为从创建开始直到程序结束。一般将静态类的唯一对象设为private
(数据隐藏,封装特性),而设计一个public
静态函数提供访问对象的唯一接口。关于类中的静态变量和静态函数的一些细节:
- 类中静态变量和静态函数都属于类,而不属于任何类的对象。换句话说,一个类只有一个静态变量和静态函数,多个对象共用这一个。
- 类的静态变量和静态函数在类中都可以直接访问,但是在类的外部必须在前面加上类名和作用域运算符
::
;- 类的静态变量在类的内部创建但是在类的外部初始化,一般在类的定义中初始化;而类的静态函数可以在类内也可以在类外初始化。 类外初始化别忘记上一点说的加上类名和
::
- 类的静态函数只能访问静态变量和静态函数,不能访问非静态变量和非静态函数。所以单例模型类中访问唯一对象的函数接口设为静态的。
- 拷贝构造函数、赋值运算符重载构造函数给禁止掉或设为私有。这通过
=delete
实现。
所以,单例模式的类示例代码如下:
// 定义一个单例模式的类
class Singleton
{
public:
// = delete 代表函数禁用,
Singleton(const Singleton& obj) = delete;//拷贝构造函禁止
Singleton& operator=(const Singleton& obj) = delete;//赋值构造函数禁止
static Singleton* getInstance();//静态函数提供访问唯一实例对象的唯一接口
private:
Singleton() = default;//构造函数设为私有,并用default强调为系统默认的构造函数
static Singleton* m_obj;//静态对象
};
饿汉模式和懒汉模式
根据实例对象被创建的时机分为饿汉模型和懒汉模式。
- 饿汉模式
// 饿汉模式
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* obj = Singleton::getInstance();
}
在饿汉模式下, 在类被加载时对象就被初始化。
- 懒汉模式
class Singleton
{
public:
// = delete 代表函数禁用,
Singleton(const Singleton& obj) = delete;//拷贝构造函禁止
Singleton& operator=(const Singleton& obj) = delete;//赋值构造函数禁止
static Singleton* getInstance();//静态函数提供访问唯一实例对象的唯一接口
{
if(m_obj == NULL)
{
m_obj = new Singleton();
return m_obj;
}
else return m_obj;
}
private:
Singleton() = default;//构造函数设为私有
static Singleton* m_obj;//静态对象
};
// 静态成员初始化放到类外部处理
Singleton* Singleton::m_obj=NULL;
int main()
{
Singleton* obj = Singleton::getInstance();
}
懒汉模式下, 单例模式的对象在类加载不去创建,在被使用时才被创建。 所以懒汉模式是更省内存的。
线程安全问题
饿汉模式是没有线程安全问题的, 因为对象在类加载时就被创建出来。多个线程不能再创建新的对象,只能通过类提供的唯一接口访问对象。
而懒汉模式会存在安全问题, 因为对象在使用时被创建。若多个线程同时使用的话可能就会创建多个对象。比如A线程第一次使用对象,使用if(m_obj == NULL)通过,下一步就是创建对象。而恰好此时时间片被线程B给占去,因为对象还为被创建,所以线程B也将开始创建对象,以此类推,所以这就可能创建多个对象,这就违背的单例模型的原则。
解决线程间数据同步的问题,最常用的办法是互斥锁。当一个线程将互斥锁锁上的时候,其他线程不能再次上锁,只能等该线程解锁后才能进行上锁和解锁的操作。换句话说, 同时只能有一个线程持有互斥锁。(一个坑位只能同时蹲一个人的意思 : )
- 互斥锁
class Singleton//互斥锁的解决方法
{
public:
// = delete 代表函数禁用,
Singleton(const Singleton& obj) = delete;//拷贝构造函禁止
Singleton& operator=(const Singleton& obj) = delete;//赋值构造函数禁止
static Singleton* getInstance();//静态函数提供访问唯一实例对象的唯一接口
{
m_mutex.lock();//锁上,其他线程给我等着
if(m_obj == NULL)//互斥锁内的区域叫做临界区,该区域为原子操作,不能操作系统分段执行。
{
m_obj = new Singleton();
return m_obj;
}
else return m_obj;
m_mutex.unlock();//解锁,其他线程开始抢锁
}
private:
Singleton() = default;//构造函数设为私有
static Singleton* m_obj;//静态对象
};
// 静态成员初始化放到类外部处理
Singleton* Singleton::m_obj=NULL;
int main()
{
Singleton* obj = Singleton::getInstance();
}
由于互斥锁的存在,所以多线程的并发性会在临界区被狠狠的限制住(一次只能有一个线程操作,其他线程被阻塞),这大大降低了代码执行的效率。
- 静态局部对象
互斥锁解决了线程安全的问题,但减低了代码执行的效率。但其实还有更好的方法可以解决线程安全的问题,那就是静态局部变量。
class Singleton
{
public:
// = delete 代表函数禁用,
Singleton(const Singleton& obj) = delete;//拷贝构造函禁止
Singleton& operator=(const Singleton& obj) = delete;//赋值构造函数禁止
static Singleton* getInstance();//静态函数提供访问唯一实例对象的唯一接口
{
static Singleton m_obj;
return &m_obj;
}
private:
Singleton() = default;//构造函数设为私有
static Singleton* m_obj;//静态对象
};
// 静态成员初始化放到类外部处理
Singleton* Singleton::m_obj=NULL;
int main()
{
Singleton* obj = Singleton::getInstance();
}
这定义了一个静态的局部单例对象,并且将这个对象作为了唯一的单例实例。使用这种方式之所以是线程安全的,是因为在 C++11 标准中有如下规定,并且这个操作是在编译时由编译器保证的:(站在巨人的肩膀上就是舒服)
如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化。
总结:
懒汉模式的缺点是在创建实例对象的时候有安全问题(可以用互斥锁或静态局部变量解决),但这样可以减少内存的浪费(如果用不到就不去申请内存了)。
饿汉模式则相反,在我们不需要这个实例对象的时候,它已经被创建出来,占用了一块内存,但它不会存在线程安全问题。文章来源:https://www.toymoban.com/news/detail-549293.html
最后用单例模式实现一个任务队列
#include <iostream>
#include <queue>
#include <mutex>
#include <thread>
using namespace std;
class TaskQueue
{
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance()//获取任务队列单例的唯一接口
{
return &m_obj;
}
// 任务队列是否为空
bool isEmpty()
{
lock_guard<mutex> locker(m_mutex);//c++11特性,相当于互斥锁的上锁和解锁
bool flag = m_taskQ.empty();
return flag;
}
// 添加任务
void addTask(int data)
{
lock_guard<mutex> locker(m_mutex);
m_taskQ.push(data);
}
// 取出一个任务
int takeTask()
{
lock_guard<mutex> locker(m_mutex);
if (!m_taskQ.empty())
{
int res = m_taskQ.front();
m_taskQ.pop();
return res;
}
return -1;
}
// 删除一个任务
bool popTask()
{
lock_guard<mutex> locker(m_mutex);
if (!m_taskQ.empty())
{
m_taskQ.pop();
return true;
}
return false;
}
private:
TaskQueue() = default;
static TaskQueue m_obj;//饿汉模式下的单例模式,类加载时就被创建,直到程序退出才清理
queue<int> m_taskQ;//维护的任务队列
mutex m_mutex;
};
TaskQueue TaskQueue::m_obj;
int main()
{
thread t1([]() {//线程1不断地往任务队列尾部添加任务
TaskQueue* taskQ = TaskQueue::getInstance();//获取类的唯一对象,该对象调用类通过的接口操作任务队列。
for (int i = 0; i < 100; ++i)
{
taskQ->addTask(i + 100);
cout << "+++push task: " << i+100 << ", threadID: " << this_thread::get_id() << endl;
this_thread::sleep_for(chrono::milliseconds(500));
}
});
thread t2([]() {//线程2不断从任务队列头部取出任务
TaskQueue* taskQ = TaskQueue::getInstance();
this_thread::sleep_for(chrono::milliseconds(100));//睡眠100ms,原子操作,不会被打断
while (!taskQ->isEmpty())
{
int data = taskQ->takeTask();
cout << "---take task: " << data << ", threadID: "
<< this_thread::get_id() << endl;
this_thread::sleep_for(chrono::seconds(1));
}
});
t1.join();//线程1结束前阻塞在这,等线程结束后释放线程1的资源
t2.join();
}
文章来源地址https://www.toymoban.com/news/detail-549293.html
//懒汉模式
#include <iostream>
#include <queue>
#include <mutex>
#include <thread>
using namespace std;
class TaskQueue
{
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue& getInstance()
{
static TaskQueue m_obj;
return m_obj;
}
// 任务队列是否为空
bool isEmpty()
{
lock_guard<mutex> locker(m_mutex);//c++11特性,相当于互斥锁的上锁和解锁
bool flag = m_taskQ.empty();
return flag;
}
// 添加任务
void addTask(int data)
{
lock_guard<mutex> locker(m_mutex);
m_taskQ.push(data);
}
// 取出一个任务
int takeTask()
{
lock_guard<mutex> locker(m_mutex);
if (!m_taskQ.empty())
{
int res = m_taskQ.front();
m_taskQ.pop();
return res;
}
return -1;
}
// 删除一个任务
bool popTask()
{
lock_guard<mutex> locker(m_mutex);
if (!m_taskQ.empty())
{
m_taskQ.pop();
return true;
}
return false;
}
private:
TaskQueue() = default;
//static TaskQueue m_obj;//饿汉模式
queue<int> m_taskQ;
mutex m_mutex;
};
//TaskQueue TaskQueue::m_obj;
int main()
{
thread t1([]() {//线程1
TaskQueue& taskQ = TaskQueue::getInstance();//获取单例任务队列
for (int i = 0; i < 100; ++i)
{
taskQ.addTask(i + 100);
cout << "+++push task: " << i+100 << ", threadID: " << this_thread::get_id() << endl;
this_thread::sleep_for(chrono::milliseconds(500));
}
});
thread t2([]() {//线程2
TaskQueue& taskQ = TaskQueue::getInstance();
this_thread::sleep_for(chrono::milliseconds(100));//睡眠100ms,原子操作,不会被打断
while (!taskQ.isEmpty())
{
int data = taskQ.takeTask();
cout << "---take task: " << data << ", threadID: "
<< this_thread::get_id() << endl;
this_thread::sleep_for(chrono::seconds(1));
}
});
t1.join();//线程1结束前阻塞在这,等线程结束后释放线程1的资源
t2.join();
}
到了这里,关于【设计模式-单例模式】的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!