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

C++线程安全单例类最全总结,第1张

#include 
#include 
#include 
// 最原始的单例模式的写法,不是线程安全的,并且会内存泄漏。
// 线程不安全的原因:假设有两个线程都执行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:
    static Singleton7* singleton;
    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的 *** 作全是读 *** 作,是线程安全的。
如果你需要在自己创建的子线程中创建单例类对象,为了保证多线程安全,可以参考我的代码写法。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/717883.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-25
下一篇 2022-04-25

发表评论

登录后才能评论

评论列表(0条)

保存