设计模式之单例模式

194 阅读4分钟

单例可以说是最简单也最常用的设计模式了。在这里总结一下单例模式。部分代码来自 [www.bilibili.com/video/BV1Vb… 李建忠)

单例模式的意义

学一个东西我们首先要知道它存在的意义,到底解决了什么问题。1. 保证了全局对象只有一个方便管理,比较典型的就是打日志的logger这种。 2. 减少了对象被反复创建再销毁带来的性能开销。

最简单的单件模式

我们先看一个最简单的没有保证线程安全的单例模式。

class singleton{
private:
        singleton(){

        }
        singletion(const singleton& ins){

        }
public:
        static singleton& getInstance(){
            static singleton* instance = nullptr;
            if(instance == nullptr){
                instance = new singleton();
            }
            return *instance;
        }
    };

其实十分简单,创建一个static的对象的指针,如果指针指向的对象还没有创建就new出来,否则直接返回对象。唯一需要注意的就是构造函数需要设置为private,这样就不会存在单件实例了。需要注意的就是这个是线程不安全的,在多线程访问的时候会出现问题。加入两个线程第一个在判断指针对象为空的时候发生调度,线程二也到了这个地方发现为nullptr就会创建出来实例。当第一个线程返回的时候就会创建一个和2不同的对象。那如何解决这个问题的呢?其实对于条件竞争的问题最常见的就是加锁处理。

线程安全的单件模式

解决的方法也十分的简单就是加锁。加完锁的代码长这个样子。

std::mutex mtx;

class singleton{
private:
        singleton(){

        }
        singletion(const singleton& ins){

        }
public:
        static singleton& getInstance(){
            lock_guard<std::mutex>lock(mtx);
            static singleton* instance = nullptr;
            if(instance == nullptr){
                instance = new singleton();
            }
            return *instance;
        }
    };

做的事情很简单,只是在判断状态的前面加一个锁而已。但是这个地方加锁会导致所有的操作串行,我们真的发生竞态条件的case只会触发一次未免有些太重。为了解决这个问题就出现了DCL(Double Check Lock)的方式来解决这个问题。

DCL单件模式

我们先看一下DCL是怎么实现的

std::mutex mtx;

class singleton{
private:
        singleton(){

        }
        singletion(const singleton& ins){

        }

public:
        static singleton& getInstance(){
            static singleton* instance = nullptr;
            if(instance == nullptr){
                lock_guard<std::mutex>lock(mtx);
                if(instance == nullptr){
                    instance = new singleton();
                }
            }
            return *instance;
        }
    };

双检查锁本质就是在加锁的区域前判断一下对象是否已经创建,如果已经创建则直接返回相关的对象。这个解决方案看似很好,但是编译器有时候会做一些指令重排工作,导致真正的机器码的执行顺序与我们设计不尽相同。我们构建一个对象正常的流程是,分配地址,构件对象,返回地址。但是经过指令重排以后可能会变成分配地址,返回地址,构造对象。这样多线程的时候一个线程拿到了非空地址以后认为对象已经构建完成,但是实际并没有就会出现问题。

使用call_once解决DCL中的问题

针对这个问题我们就需要保证不发生指令重排或者保证只初始化一次。C++有一个call_once方法,保证了在多线程情况下对应的函数只会被调用一次。而且如果有人在active状态(就是已经调用了但是没有返回),其他的passive会同步等待,不会出现上面DCL的问题。下面的话来自cppreference

The return from the returning call synchronizes-with the returns from all passive calls on the same flag: this means that all concurrent calls to call_once are guaranteed to observe any side-effects made by the active call, with no additional synchronization.

我们就有这种方法来写一下。

std::once_flag flag;

class singleton{
private:
    singleton(){}
    singleton(const singleton& s){}
public:
    static singleton& getInstace(){
        static singleton* instance = nullptr;
        if(instance == nullptr){
            call_once(flag, [=](){
                instance = new singleton();
            });
        }
        return *instance;
    }
};

这样就可以解决DCL中存在的问题了。这个call_once在里面使用了atomic的load state的方法,保证了即使发生了指令重排也不会有问题。

C++11以后使用静态局部变量写单件

这个也是看了面经才知道的事情,C++11以后规范要求了对静态局部变量的初始化是线程安全的。

下面是C++11的标准, 加粗的部分是标准对静态局部变量的初始化要求。

such a variable is initialized the first time control passes through its declaration; such a variable is considered initialized upon the completion of its initialization. If the initialization exits by throwing an exception, the initialization is not complete, so it will be tried again the next time control enters the declaration. If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization. If control re-enters the declaration recursively while the variable is being initialized, the behavior is undefineded.

所以我们写这种方式在C++11的情况下就是线程安全的。

class singleton{
private:
    singleton(){}
    singleton(const singleton& s){}
public:
    static singleton& getInstace(){
        static singleton instance;
        return instance;
    }
};

具体编译器实现的细节可以看这篇文章: blog.csdn.net/imred/artic…