C++多线程:单例模式与共享数据安全(七)

1、单例设计模式
  • 单例设计模式,使用的频率比较高,整个项目中某个特殊的类对象只能创建一个

  • 并且该类只对外暴露一个public方法用来获得这个对象。

  • 单例设计模式又分懒汉式和饿汉式,同时对于懒汉式在多线程并发的情况下存在线程安全问题

    • 饿汉式:类加载的准备阶段就会将static变量、代码块进行实例化,最后只暴露一个public方法获得实例对象。

    • 懒汉式:当需要用到的时候再去加载这个对象。这时多线程的情况下可能存在线程安全问题

  • 对于饿汉式这里不做具体的解释,本节只讨论多线程与懒汉式的线程安全问题

2、单线程下的懒汉模式
2.1、单例对象的创建:
  • 将类指针对象进行静态私有化,并且在类外初始化这个对象为空;静态能保证的是这个对象属于这个类不属于任何一个对象
  • 私有化空构造器防止可以实例化对象
  • 对外暴露一个public方法获取该对象,如果在获取时发现该对象为空,那么进行实例化,否则直接返回
  • 因此可以看到实例化只有一次,多次获取到的对象的地址属于同一个
cpp 复制代码
class Single_Instance {
private:
    static Single_Instance *instance;
    Single_Instance() {

    }
public:
    static Single_Instance *get_Instance(){
        if(instance == NULL){
            instance = new Single_Instance();
        }
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;

void test1()
{
    Single_Instance *instance1 = Single_Instance::get_Instance();
    Single_Instance *instance2 = Single_Instance::get_Instance();
    instance1->func();
    instance2->func();
}

# 输出
func(), &instance = 0x5652eefede70
func(), &instance = 0x5652eefede70
2.2、单例对象的析构
  • 很明显上面的代码缺少一个析构函数,并且似乎无从下手找一个合适的时机对其进行析构,只能等待程序运行结束操作系统回收?
  • 其实可以通过内部类的方式进行析构
    • 首先在单例类内部进行私有化一个内部类
    • 对外暴露的public获取instance的对象接口在new实例化对象的时候创建一个内部类静态成员
    • 内部类静态成员的好处是只有一份
    • 当作用域结束时内部类就会负责析构掉主类的静态成员对象
cpp 复制代码
class Single_Instance {
private:
    static Single_Instance *instance;
    Single_Instance() {

    }
    class inner_class {
    public:
        ~inner_class(){
            if(Single_Instance::instance){
                delete Single_Instance::instance;
                Single_Instance::instance = NULL;
                std::cout << "inner_class::~inner_class(), 析构Single_Instance::instance对象" << std::endl;
            }
        }
    };
public:
    static Single_Instance *get_Instance(){
        if(instance == NULL){
            instance = new Single_Instance();
            static inner_class innerClass;
        }
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;

void test1()
{
    Single_Instance *instance1 = Single_Instance::get_Instance();
    Single_Instance *instance2 = Single_Instance::get_Instance();
    instance1->func();
    instance2->func();
}
#输出
func(), &instance = 0x558eb768de70
func(), &instance = 0x558eb768de70
inner_class::~inner_class(), 析构Single_Instance::instance对象
3、单例模式与多线程
  • 单例模式的对象可能会被多个线程使用,但是又必须保证这个单例的对象只有一份

  • 不能重复创建、也必须保证这个对象在多线程使用过程中不会因为创建而产生数据安全问题,即多线程抢占的创建这一个对象

cpp 复制代码
class Single_Instance {
private:
    static Single_Instance *instance;
    Single_Instance() {

    }
    class inner_class {
    public:
        ~inner_class(){
            if(Single_Instance::instance){
                delete Single_Instance::instance;
                Single_Instance::instance = NULL;
                std::cout << "inner_class::~inner_class(), 析构Single_Instance::instance对象" << std::endl;
            }
        }
    };
public:
    static Single_Instance *get_Instance(){
        if(instance == NULL){
            instance = new Single_Instance();
            static inner_class innerClass;
        }
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;

void thread_func()
{
    std::cout << "子线程开始执行了" << std::endl;
    Single_Instance *instance = Single_Instance::get_Instance();
    std::cout << "thread_func, &instance = " << instance << std::endl;
    std::cout << "子线程执行结束了" << std::endl;
}

void test2()
{
    std::thread mythread1(thread_func);
    std::thread mythread2(thread_func);
    std::thread mythread3(thread_func);
    std::thread mythread4(thread_func);
    mythread1.join();
    mythread2.join();
    mythread3.join();
    mythread4.join();
}

可以看到实例化不止一个单例对象,这一现象违反了单例的思想,因此需要在多线程抢占创建时进行互斥(mutex)

3.1、解决方案(一)
  • 使用互斥量的方式,对线程访问获取对象进行阻塞
  • 但是不难发现问题,其实这个对象只创建一次,之后的访问单纯的获取这个对象也要进行加锁逐个排队访问临界区,这一现象导致效率极低
cpp 复制代码
std::mutex mutex_lock;
class Single_Instance {
private:
    static Single_Instance *instance;
    Single_Instance() {

    }
    class inner_class {
    public:
        ~inner_class(){
            if(Single_Instance::instance){
                delete Single_Instance::instance;
                Single_Instance::instance = NULL;
                std::cout << "inner_class::~inner_class(), 析构Single_Instance::instance对象" << std::endl;
            }
        }
    };
public:
    static Single_Instance *get_Instance(){
        std::unique_lock<std::mutex> uniqueLock(mutex_lock);
        if(instance == NULL){
            instance = new Single_Instance();
            static inner_class innerClass;
        }
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;

void thread_func()
{
    std::cout << "子线程开始执行了" << std::endl;
    Single_Instance *instance = Single_Instance::get_Instance();
    std::cout << "thread_func, &instance = " << instance << std::endl;
    std::cout << "子线程执行结束了" << std::endl;
}

void test2()
{
    std::thread mythread1(thread_func);
    std::thread mythread2(thread_func);
    std::thread mythread3(thread_func);
    std::thread mythread4(thread_func);
    mythread1.join();
    mythread2.join();
    mythread3.join();
    mythread4.join();
}
3.2、解决方式(二)

双重检查机制(DCL)进行绝对安全解决

  • 双重检查:
    • 首先在锁外面加入一个if判断,判断这个对象是否存在,如果存在就没有必要上锁创建,直接返回即可
    • 如果对象不存在,首选进行加锁,然后在if判断对象是否存在,这个if的意义在于当多个线程阻塞在mutex锁头上时
    • 突然有一个线程1创建好了,那么阻塞在mutex锁头上的线程2、3、4...都不用再继续创建,因此在加一个if判断

这里还需要解释一下volatile关键字:

  • volatile关键字的作用是防止cpu指令重排序,重排序的意思就是干一件事123的顺序,cpu可能重排序为132

  • 为什么需要防止指令重排序,因为对象的new过程分为三部曲:

    (1)分配内存空间、(2)执行构造方法初始化对象、(3)将这个对象指向这个空间;

    由于程序运行CPU会进行指令的重排序,如果执行的指令是132顺序,A线程执行完13之后并没有完成对象的初始化、而这时候转到B线程;B线程认为对象已经实例化完毕、其实对象并没有完成初始化!产生错误

  • 但这个问题在C++11中已经禁止了重排序,因此不需要使用volatile关键字,但在Java和一些其他语言中可能有,Java中这个关键字是针对即时编译器JIT进行指令重排序的

cpp 复制代码
static Single_Instance *get_Instance(){
    if(instance == NULL){
        std::unique_lock<std::mutex> uniqueLock(mutex_lock);
        if(instance == NULL){
            instance = new Single_Instance();
            static inner_class innerClass;
        }
    }
    return instance;
}

只需要把上面的代码改成这个样子即可

4、std::call_once()
  • std::call_once()是C++11引入的函数,该函数的功能就是保证一个方法只会被调用一次。

    • 参数二:一个函数名func

    • 参数一:std::once_flag一个标记,本质是一个结构体。该标志可以用于标记参数二该函数是否已经调用过了

    • 参数三:参数二函数的参数

  • std::call_once()具有互斥量的这种能力,且效率上比mutex互斥量效率更高,因此也可以使用这个函数对单例的线程安全进行保证

    • 当call_once调用过一次之后,std::once_flag将会被修改标记(已调用),那么之后都不会在调用
  • 下面看个代码举例,可以看到create_Instance()函数中对于这个函数只执行了一次,完全ojbk。

cpp 复制代码
class Single_Instance {
private:
    static Single_Instance *instance;
    static std::once_flag instance_flag;
    Single_Instance() {

    }
    class inner_class {
    public:
        ~inner_class(){
            if(Single_Instance::instance){
                delete Single_Instance::instance;
                Single_Instance::instance = NULL;
                std::cout << "inner_class::~inner_class(), 析构Single_Instance::instance对象" << std::endl;
            }
        }
    };
public:

    static void create_Instance(){
        instance = new Single_Instance();
        static inner_class innerClass;
    }
    static Single_Instance *get_Instance(){
        std::call_once(instance_flag, create_Instance);
        return instance;
    }
    void func(){
        std::cout << "func(), &instance = " << instance << std::endl;
    }
};
Single_Instance *Single_Instance::instance = NULL;
std::once_flag Single_Instance::instance_flag;

void thread_func()
{
    std::cout << "子线程开始执行了" << std::endl;
    Single_Instance *instance = Single_Instance::get_Instance();
    std::cout << "thread_func, &instance = " << instance << std::endl;
    std::cout << "子线程执行结束了" << std::endl;
}

void test3()
{
    std::thread mythread1(thread_func);
    std::thread mythread2(thread_func);
    std::thread mythread3(thread_func);
    std::thread mythread4(thread_func);
    mythread1.join();
    mythread2.join();
    mythread3.join();
    mythread4.join();
}
相关推荐
矛取矛求2 小时前
Linux如何更优质调节系统性能
linux
李元豪3 小时前
【智鹿空间】c++实现了一个简单的链表数据结构 MyList,其中包含基本的 Get 和 Modify 操作,
数据结构·c++·链表
UestcXiye3 小时前
《TCP/IP网络编程》学习笔记 | Chapter 9:套接字的多种可选项
c++·计算机网络·ip·tcp
内核程序员kevin3 小时前
在Linux环境下使用Docker打包和发布.NET程序并配合MySQL部署
linux·mysql·docker·.net
kayotin4 小时前
Wordpress博客配置2024
linux·mysql·docker
一丝晨光4 小时前
编译器、IDE对C/C++新标准的支持
c语言·开发语言·c++·ide·msvc·visual studio·gcc
丶Darling.4 小时前
Day40 | 动态规划 :完全背包应用 组合总和IV(类比爬楼梯)
c++·算法·动态规划·记忆化搜索·回溯
Ztiddler4 小时前
【Linux Shell命令-不定期更新】
linux·运维·服务器·ssh
小小不董4 小时前
Oracle OCP认证考试考点详解082系列16
linux·运维·服务器·数据库·oracle·dba
奶味少女酱~5 小时前
常用的c++特性-->day02
开发语言·c++·算法