设计模式学习总结:(10)单例模式探讨

单例模式(Singleton)很简单,从名字也很容易知道解决的是唯一对象创建问题,很多时候,如果因为一个对象只需要存在一份,正常对象创建方式有种杀鸡用牛刀的感觉。同时,也不能假设用户素质足够高,至少我们要保证从语法上,多个对象存在是不合理的,我们所要做的,就是约束使用者的行为。

意图:

保证一个类仅有一个实例,并提供一个全局访问点。


在c++中为了限定对象的创建,我们需要把构造函数设置为私有,保证无法从外界构造,同时需要一个静态变量指针来保存唯一对象,最后至少还需要一个函数来获得这个唯一对象。

class Singleton{
private:
    Singleton()=default;
    
    static Singleton* _instance;
public:
    static Singleton* getInstance();
    
};

Singleton* Singleton::m_instance=nullptr; //c++11 nullptr

Singleton* Singleton::getInstance() {
    if (_instance == nullptr) {     //多线程触发点
        _instance = new Singleton();
    }
    return _instance;
}

李建忠老师的设计模式这里让我大开眼界,送上笔记一枚:

-------------------------------------------------------------note------------------------

这种实现方式在单线程下,已经很好了,但是再多线程下,存在安全隐患。

在多线程情况下,如果多个线程同时执行到  if  判断那里,当第一个线程进入判断,然后第二个线程也进入判断,依次类推,可能有多个线程进入判断,这样就造成多个实例被new出来,而最终只有一个能被获得,剩下的将成为内存泄露的一分子。所以,我们很自然的想到用线程锁来解决,假设有这样一个线程锁,我们可以这样实现:

Singleton* Singleton::getInstance() {
    Lock lock;
    
    if (_instance == nullptr) {
        _instance = new Singleton();
    }

lock.relase();
return _instance; }

但是又考虑到,if判断,只有有限的一次可能执行到,剩下大超级大的一部分是不可能进入判断的,也就是说,仅仅为了有限的o(1)次,我们就每一次创建一个锁,然后释放,如果次数足够多,并发量足够大,效率有很大的影响。很自然是不允许的,对于很多需要效率的场景,需要有更好的做法,所以有了曾经风靡一时的双重锁解法。

Singleton* Singleton::getInstance() {
    
    if(_instance==nullptr)
    {
        Lock lock;
        if (_instance == nullptr) 
        {
            _instance = new Singleton();
        }
        lock.realse();
    }
    return _instance;
}

保证了在足够多次的情况下,都不会获得锁,即使有幸获得锁,我们在进行判断,防止漏网之鱼,其实这代码从高级语言层面上看已经很完美了,然而,这里面竟然存在安全隐患。

叫做内存reorder隐患。 参考资料:链接

简单的说,_instance = new Singleton() 我们这一句的理想顺序是 创建空间->构造对象->赋值给指针。但是底层基于效率考虑,可能会编译成这样一种执行顺序,就是 创建空间->赋值给指针->构造对象。这样就造成如果刚好某个线程执行到了,赋值给指针,这一步,然后切换到另一个线程它判断,发现指针不是null,于是它就直接返回对象,注意,这个时候它返回了一个还未构造属性的对象。这就是问题所在。

最后,附上c++11新标准的解决方案,直接复制代码过来。

std::atomic<Singleton*> Singleton::_instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
    Singleton* tmp = _instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = _instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;
            std::atomic_thread_fence(std::memory_order_release);//释放内存fence
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}

--------------------end----------------------------

下面写一些见解,纯思考:

----------------------------------------------------------语法思考-------------------------------------------------------------------------------------

c++类里面,static 可以定义成指针,也能定义成普通的成员。

抛出疑问:

class Singleton
{
    Singleton()=default;
    static Singleton _instance;
public:
    static Singleton getInstance();
};
Singleton Singleton::_instance = Singleton();
Singleton Singleton::getInstance()
{
    return _instance;
}

这种方式好像也能解决多线程问题,我不知道存不存在安全隐患,暂且保留,后续补充。

另外,我们对比一下一个类里面如果这三种定义:

class A
{
static A a; //合理
static A *b; //合理

A *c; //合理
A d;//不合理
}

很正常的语法,之前也做过思考,我们只要知道第四个为什么不合理,一切都明了了。在类实例被创建的时候,如果是第四种形式,你可以想象,这样的定义是需要默认初始化,或者如果你给它一个类。但是他本身是一个A实例,这个A实例也存在这样的一个A实例,于是A实例里面有个A1实例,A1实例里面有个A2实例,何时是终结。就好像山上有个庙,庙里有个和尚,和尚说:“山上有个庙。。。”。

如果是指针,那么可以默认初始化为null,这样null就不存在A实例,就不会进入和尚的庙。至于静态变量,如果你理解它的内存空间,其实不管是任何实例,都是同一个A实例,是唯一的同一个。基于此,我做过一个耐人寻味的测试:

class A
{
static A a;
public:
    int b;
}
A A::a = A();
int main()
{
    A test = A();
    test.a.a.a.a.a.b = 1;
    cout << test.a.b<<endl;
    test.a.a.a.a.a.a.a.a.a.a.a.b = 2;
    cout << test.a.b<<endl;
   return 0;
}

能猜到结果吗。

------------------------------------------------------------end--------------------------------------------------------------------------------------

原文地址:https://www.cnblogs.com/wuweixin/p/5452123.html