C++线程安全单例类最全总结

本文详细介绍了C++中实现线程安全单例模式的多种方法,包括非线程安全的原始单例、采用类中类解决内存泄漏、饿汉式和懒汉式写法。特别讨论了懒汉式单例的线程安全问题,提出了双重锁定解决方案,并指出C++11之后静态局部对象是实现线程安全单例的最佳实践。文章还提供了多线程环境下使用单例的注意事项。
摘要由CSDN通过智能技术生成
#include <thread>
#include <iostream>
#include <mutex>
// 最原始的单例模式的写法,不是线程安全的,并且会内存泄漏。
// 线程不安全的原因:假设有两个线程都执行getInstance函数。当线程1调用singleton = new Singleton1()
// 语句时候,操作系统系统突然切换到线程2,线程2判断if (singleton == nullptr)生效,线程2执行
// singleton = new Singleton1();当线程2执行完后,singleton已经生成。然后切换到线程1,线程1继续执行
// singleton = new Singleton1(),singleton会再次生成。这不符合单例设计的原则。
// 内存泄漏的原因:析构函数没法调用,所以无法通过析构函数调用delete,删除singleton内存

class Singleton1
{
public:
    ~Singleton1() {std::cout << "Singleton1析构函数调用" << std::endl;} // 析构函数其实不会调用,所以new出来的静态成员变量会内存泄漏。
    static Singleton1* getInstance()
    {
        if (singleton == nullptr)
        {
            singleton = new Singleton1();
        }
        return singleton;
    }
    void func()
    {
        printf("调用func函数\n");
    }
private:
    // static函数只能调用静态成员变量或者静态函数,所以下面这个静态成员变量必须为static 
    static Singleton1* singleton;
    Singleton1(){}
};
// 静态非const整形成员变量必须在类外定义
Singleton1* Singleton1::singleton = nullptr;

// 再写个单例模式,采用类中类解决内存泄露的问题。其实在main函数中也可以手动delete,只不过不是很优雅。
class Singleton2
{
private:
    Singleton2(){}
    static Singleton2* singleton;
public:
    ~Singleton2() 
    {
        std::cout << "Singleton2析构函数调用" << std::endl;
    } // 析构函数其实不会调用,所以new出来的静态成员变量会内存泄漏。
    static Singleton2* getInstance()
    {
        if (singleton == nullptr)
        {
            singleton = new Singleton2();
            static PtrCycle ptr; // C++能确保静态变量只能被初始化一次,不会因为调用getInstance,多次创建静态对象。
        }
        return singleton;
    }
    void func()
    {
        printf("调用func函数\n");
    }

private:
    class PtrCycle
    {
    public:
        ~PtrCycle()
        {
            if (singleton)
            {
                delete singleton; //这里会调用析构函数
                singleton = nullptr;
                std::cout << "释放内存" << std::endl;
            }
        }
    };
};
Singleton2* Singleton2::singleton = nullptr; //必须要在类外初始化

// 上面的写法还是线程不安全的。为了解决线程安全,引申出下面的饿汉式和懒汉式写法。
// 饿汉式:一开始就初始化单例对象
// 饿汉式写法一:把对象用new放在堆上。
class Singleton3
{
private:
    static Singleton3* singleton;
    Singleton3(){}
public:
    void func()
    {
        printf("调用func函数\n");
    }
    static Singleton3* getInstance()
    {
        static PtrCycle ptr;
        return singleton;
    }
    ~Singleton3(){std::cout << "Singleton3析构函数" << std::endl;}
private:
    class PtrCycle
    {
    public:
        ~PtrCycle()
        {
            if (singleton)
            {
                delete singleton; // 这里会调用析构函数
                singleton = nullptr;
                std::cout << "释放内存" << std::endl;
            }
        }
    };
};
Singleton3* Singleton3::singleton = new Singleton3(); //静态对象类外初始化,其实这个写法不好
// 上面new出来的这个指针,如果getInstace函数从没被调用过,那么因为new Singleton3()
// 得到的内存从没被释放,会发生内存泄漏。

// 饿汉式写法二:把对象放在静态区,不使用new。
// 这种写法不需要写类中类去释放内存,或者在main函数中手动删除内存
class Singleton4
{
private:
    static Singleton4 singleton;
    Singleton4(){}
public:
    void func()
    {
        printf("调用func函数\n");
    }
    ~Singleton4(){std::cout << "Singleton4析构函数" << std::endl;}
    static Singleton4* getInstance()
    {
        return &singleton;
    }
};
Singleton4 Singleton4::singleton; // 静态对象类外初始化
// 饿汉式的总结
// 由于在定义静态变量的时候实例化单例类,因此在类加载的时候就已经创建了单例对象,可确保单例对象的唯一性。线程是安全的。
// 缺点:无论系统运行时是否需要使用该单例对象,都会在类加载时创建对象,资源利用效率不高。

// 懒汉式:需要时候再实例化单例对象。
// 懒汉式1:直接加个锁。
// 这样的代码其实有个很严重的问题,就是代码中可能需要频繁调用getInstance这个函数
// 因为只有借助getInstace这个函数才能获取到单例类对象,然后才能调用单例类的其他成员
// 函数。为了解决一个初始化该类对象的互斥问题,居然在getInstace里面加了互斥量。导致
// 所有时刻,调用getInstance这个函数,都会因为锁互斥一下,严重影响性能。因为除了初始化时刻,其他
// 时候完全不需要互斥。一旦初始化完成,if (singleton == nullptr)永远不会成立,所以singleton = new Singleton()
// 永远不会再次执行。
class Singleton5
{
private:
    static Singleton5* singleton;
    static std::mutex my_mutex; //这里用的静态成员变量,保证所有用到这个类的,用的是同一个互斥量。当然定义一个全局互斥量也可以。
    Singleton5(){}
    class PtrCycle
    {
    public:
        ~PtrCycle()
        {
            if (singleton)
            {
                delete singleton; //这里会调用析构函数
                singleton = nullptr;
                std::cout << "释放内存" << std::endl;
            }
        }
    };

public:
    ~Singleton5(){std::cout << "Singleton5析构函数执行" << std::endl;}
    static Singleton5* getInstance()
    {
        std::lock_guard<std::mutex> my_guard(my_mutex);
        if (singleton == nullptr)
        {
            singleton = new Singleton5();
            static PtrCycle ptr;
        }
        return singleton;
    }
    void func()
    {
        printf("调用func函数\n");
    }

};
std::mutex Singleton5::my_mutex;
Singleton5* Singleton5::singleton = nullptr;

// 懒汉式2:双重锁定
// 双重锁定的写法,保证线程1在if (singleton == nullptr)成立之后,
// singleton = new Singleton6();运行之前,一定不会发生上下文的切换。
// 因此会创建完成单例类对象。然后互斥量解锁之后,哪怕发生上下文切换,换到了另一个
// 线程,此时if (singleton == nullptr)一定不会成立,因此不会再调用第二次 singleton = new Singleton6()。
// 初始化时候,需要用到这个互斥量加锁,其他时候并不会用到这个互斥量。因为一旦初始化完成之后
// if (singleton == nullptr)一定不会成立,因此不会因为调用一次getInstance就创建一次互斥量。
// 因此大大提升了代码的运行效率。


class Singleton6
{
private:
    static Singleton6* singleton;
    static std::mutex my_mutex;
    Singleton6(){}
    class PtrCycle
    {
    public:
        ~PtrCycle()
        {
            if (singleton)
            {
                delete singleton; //这里会调用析构函数
                singleton = nullptr;
                std::cout << "释放内存" << std::endl;
            }
        }
    };

public:
    ~Singleton6(){std::cout << "Singleton6析构函数执行" << std::endl;}
    static Singleton6* getInstance()
    {
        if (singleton == nullptr)
        {
            std::lock_guard<std::mutex> my_guard(my_mutex);
            if (singleton == nullptr)
            {
                singleton = new Singleton6();
                static PtrCycle ptr;
            }
        }
        return singleton;
    }
    void func()
    {
        printf("调用func函数\n");
    }

};
std::mutex Singleton6::my_mutex;
Singleton6* Singleton6::singleton = nullptr;

// C++11之后,静态局部对象是实现多线程安全的单例类最佳写法。
// C++11之后,多个线程同时初始化一个同一局部静态对象,可以保证只初始化一次。
//  在实现单例的过程中要注意如下问题:
// 1. 构造函数应该声明为非公有,从而禁止外界创建实例。
// 2. 拷贝操作和移动操作也应该禁止。
// 3. 只能通过 Singleton 的公有特定类操作访问它的唯一实例(C++中的一个公有静态成员函数)
class Singleton7
{
public:
    ~Singleton7(){std::cout << "Singleton7析构函数执行" << std::endl;}
    static Singleton7* getInstance()
    {
        static Singleton7 singleton_tmp;
        return &singleton_tmp;
    }
    void func()
    {
        printf("调用func函数\n");
    }

private:
    Singleton7(){}
    // 拷贝构造函数
    Singleton7(const Singleton7& singleton) = delete;
    // 拷贝赋值函数
    Singleton7& operator = (const Singleton7& singleton) = delete;
    // 移动构造函数
    Singleton7(Singleton7&& singleton) = delete;
    // 移动赋值构造函数
    Singleton7& operator = (Singleton7&& singleton) = delete;
};


void my_thread()
{
    printf("thread run\n");
    Singleton3* s = Singleton3::getInstance();
    printf("address is: %p \n", s);
    s->func();
}

int main()
{
    std::thread my_thread1(my_thread);
    std::thread my_thread2(my_thread);
    my_thread1.join();
    my_thread2.join();
    return 0;
}

不过有一点需要说明的是:将单例类放在主线程中,在其他子线程创建并运行之前,将单例类初始化完成是强烈推荐的。这样就不存在多个子线程对这个单例类对象访问的冲突问题,因为一旦初始化完成,再次调用getInstance的操作全是读操作,是线程安全的。
如果你需要在自己创建的子线程中创建单例类对象,为了保证多线程安全,可以参考我的代码写法。

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
线程安全单例模式是一种在多线程环境下确保只有一个实例被创建的设计模式。在实际使用中,有时需要保证某个的对象在整个应用程序中只能有一个实例存在,并且该实例在任何时候都能被访问和使用。 线程安全单例模板的实现一般包含以下几个要点: 1. 构造器私有化:通过将的构造函数私有化,可以防止在外部直接实例化对象。 2. 静态成员对象:在内部创建一个私有的静态成员对象,用于存储的唯一实例。 3. 全局访问方法:通过提供一个公共的静态方法,来获取的唯一实例。在该方法内部进行实例化操作,保证只有一个实例被创建。 4. 线程安全性:由于多线程环境下会有多个线程同时访问该方法,需要考虑线程安全问题。可以通过加锁机制,或者使用双重检查锁定(double-checked locking)来保证线程安全。 双重检查锁定是一种常用的实现方式,具体步骤如下: 1. 在全局访问方法内进行第一次判断,检查实例是否已经被创建。 2. 若实例为空,则进行同步锁定,防止其他线程同时进入。 3. 在同步块内再次检查实例是否为空,如果为空则进行实例化。 4. 返回实例。 这样可以确保在多线程环境下,只有一个实例被创建,同时保证访问的效率和线程安全性。 总之,线程安全单例模板是一种重要的设计模式,它可以保证在多线程环境下只有一个实例存在,并且正常进行访问。通过适当的加锁机制,可以确保线程安全性,保护对象的一致性和可靠性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值