【设计模式-单例模式】

什么是单例模式?

在一个项目中的全局范围内, 一个类有且仅有一个实例对象。这个唯一的实例对象给其他模块提供数据的全局访问。这样的模式就叫单例模式
单例模式的典型例子就是任务队列。

那么如何去实现这样的一个单例模式的类?

首先, 考虑单例模式的要求为有且仅有一个实例对象。那么就先从构造函数入手。类的构造函数主要有:构造函数、拷贝构造函数、赋值运算符重载构造函数。

  • 对于构造函数,将构造函数的访问权限设为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 标准中有如下规定,并且这个操作是在编译时由编译器保证的:(站在巨人的肩膀上就是舒服)
如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化。

总结:

懒汉模式的缺点是在创建实例对象的时候有安全问题(可以用互斥锁或静态局部变量解决),但这样可以减少内存的浪费(如果用不到就不去申请内存了)。
饿汉模式则相反,在我们不需要这个实例对象的时候,它已经被创建出来,占用了一块内存,但它不会存在线程安全问题。

最后用单例模式实现一个任务队列

#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();
}

在这里插入图片描述

//懒汉模式
#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();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值