美文网首页
设计模式之单例模式(未完待续)

设计模式之单例模式(未完待续)

作者: coolTigers | 来源:发表于2019-12-22 17:07 被阅读0次

    单例模式的意图是:“保证一个类仅有一个实例,并提供一个访问它的全局访问点。--《DP》”
    从单例模式的目的来看,类的构造函数不能是public属性的,不然使用者都可以调用,也就不止一个实例对象。提供一个全局访问点意味着需要一个静态的访问函数。其实这里的实现思想也非常朴素,既然只能有一个实例,那么就在构造前判断一下是否存在对象,存在就直接返回,不存在就构造。是不是非常类似于微机原理中标志位的感觉。那么上代码:
    singleton.h

    #pragma once
    
    #ifndef SINGLETON_H
    #define SINGLETON_H
    class Singleton {
    public:
        // 暴露给外部的全局访问点
        static Singleton* GetInstance();
    protected:
        
    private:
        static Singleton* instance;
        Singleton();
        Singleton(Singleton& s);
        Singleton operator = (const Singleton);
    };
    #endif // !SINGLETON_H
    
    

    singleton.cpp

    #include "singleton.h"
    
    Singleton* Singleton::instance = nullptr;
    
    Singleton* Singleton::GetInstance()
    {
        if (instance == nullptr) {
            instance = new Singleton();  
        }
        return instance;
    }
    
    Singleton::Singleton()
    {
    }
    

    验证如下:

    #include "singleton.h"
    #include <iostream>
    
    using namespace std;
    
    int main()
    {
        Singleton* s1 = Singleton::GetInstance();
        Singleton* s2 = Singleton::GetInstance();
    
        cout << "s1: " << s1 << endl;
        cout << "s2: " << s2 << endl;
    
        return 0;
    }
    
    

    运行结果如下:


    image.png

    可以看出,虽然想构造两个对象,但是指向的都是同一个实例对象。

    如上代码在单线程场景下,功能是没有问题的,反之,多线程则会出现问题。它不是线程安全的实现。
    假设线程1运行到Singleton* Singleton::GetInstance()函数if分支中,假设此时instance=nullptr,并且判断完毕,此时CPU时间片切到线程2,恰好也是这个函数,由于此时instance的值是nullptr,那么会new一个实例。再次切换到线程1,执行new实例操作,结果就创建了两个不同的实例。
    现在来修改下GetInstance函数:

    Singleton* Singleton::GetInstance()
    {
        Lock lock;
        if (instance == nullptr) {
            instance = new Singleton();  
        }
        return instance;
    }
    

    这里的实现可以避免上述的问题。但是这个实现也不是很合理,上述问题出现的前提是instance=nullptr,没必要进来就加锁,而且高并发的情况下影响性能。检查后再加锁的做法被称为“double-check locking”(双重锁定或双检查锁)。

    Singleton* Singleton::GetInstance()
    {
        if (instance == nullptr) {
            Lock lock;
            if (instance == nullptr) {
                instance = new Singleton();
            }
            return instance; 
        }
    
    }
    

    双重锁定的实现在通常情况下似乎没有问题,但是假如编译器做了一些“优化”:reorder,上述代码依然有漏洞。
    让我们看下instance = new Singleton()语句的具体实现:
    1、申请内存;
    2、调用Singleton构造器;
    3、把内存地址传给instance;
    如果编译器进行优化后,变成如下顺序:
    1、申请内存;
    2、把内存地址传给instance;
    3、调用Singleton构造器;
    假设线程1执行完申请内存,并且把内存地址传给instance(即instance != nullptr)后,挂起,线程2执行到GetInstance函数,此时(instance == nullptr)的结果是个false,直接返回地址。使用这个指针就会出现问题,因为实际上它还是个原生指针,还没有被类构造!
    关于如何修改,李建忠给出了一种想法,我觉得太繁琐了。地址在https://www.bilibili.com/video/av24176315?t=1895&p=12 P12部分,时间26:00。
    后续继续更新。

    相关文章

      网友评论

          本文标题:设计模式之单例模式(未完待续)

          本文链接:https://www.haomeiwen.com/subject/dpmonctx.html