设计模式知识总结

时间:2024-04-16 15:10:01

单例模式

懒汉式

线程不安全的懒汉单例

class singleton {
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
    void st();
};
singleton *singleton::p = nullptr;
singleton* singleton::instance() 
{
    if (p == nullptr)
        p = new singleton();
    return p;
}

//访问方式:
class test
{
public:

private:
	singleton  *sing;
};

test::test()
{
	sing = singleton::instance();
	sing.st();  //访问方式1
	//or
	singleton::instance().st(); //访问方式2
}

这是一个非常简单的实现,将构造函数声明为private或protect防止被外部函数实例化,内部有一个静态的类指针保存唯一的实例,实例的实现由一个public方法来实现,该方法返回该类的唯一实例。
当然这个代码只适合在单线程下,当多线程时,是不安全的。考虑两个线程同时首次调用instance方法且同时检测到p是nullptr,则两个线程会同时构造一个实例给p,这将违反了单例的准则。

使用锁

class singleton 
{
private:
    singleton() {}
    static singleton *p;
    static mutex lock_;
public:
    static singleton *instance();
};

singleton *singleton::p = nullptr;
singleton* singleton::instance() 
{
    lock_guard<mutex> guard(lock_);
    if (p == nullptr)
        p = new singleton();
    return p;
}

这种写法不会出现上面两个线程都执行到p=nullptr里面的情况,当线程A在执行p = new Singleton()的时候,线程B如果调用了instance(),一定会被阻塞在加锁处,等待线程A执行结束后释放这个锁。从而是线程安全的。
但是这种写法性能非常低下,因为每次调用instance()都会加锁释放锁,而这个步骤只有在第一次new Singleton()才是有必要的,只要p被创建出来了,不管多少线程同时访问,使用if (p == nullptr) 进行判断都是足够的(只是读操作,不需要加锁),没有线程安全问题,加了锁之后反而存在性能问题。
因此引出DCL。

双重检查锁

class singleton 
{
private:
    singleton() {}
    static singleton *p;
    static mutex lock_;
public:
    singleton *instance();
    // 实现一个内嵌垃圾回收类
    class CGarbo
    {
    public:
        ~CGarbo()
        {
            if(singleton::p)
                delete singleton::p;
        }
    };
    static CGarbo Garbo; // 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
};
singleton *singleton::p = nullptr;
singleton::CGarbo Garbo;
singleton* singleton::instance() 
{
    if (p == nullptr) 
    {
        lock_guard<mutex> guard(lock_);
        if (p == nullptr)
            p = new singleton();
    }
    return p;
}

双重检查锁在c++11之前会出现的问题

DCLP的关键在于,大多数对instance的调用会看到p是非空的,因此甚至不用尝试去初始化它。因此,DCLP在尝试获取锁之前检查p是否为空。只有当检查成功(也就是p还没有被初始化)时才会去获得锁,然后再次检查p是否仍然为空(因此命名为双重检查锁)。第二次检查是必要,因为就像我们刚刚看到的,很有可能另一个线程偶然在第一次检查之后,获得锁成功之前初始化p。
看起来上述代码非常美好,可是过了相当一段时间后,才发现这个漏洞,原因是:内存读写的乱序执行(编译器问题)。
再次考虑初始化p的那一行:

p = new singleton;

这条语句会导致三个事情的发生:

  1. 分配能够存储singleton对象的内存;
  2. 在被分配的内存中构造一个singleton对象;
  3. 让p指向这块被分配的内存。

可能会认为这三个步骤是按顺序执行的,但实际上只能确定步骤1是最先执行的,步骤2,3却不一定。问题就出现在这。
线程A调用instance,执行第一次p的测试,获得锁,按照1,3,执行,然后被挂起。此时p是非空的,但是p指向的内存中还没有Singleton对象被构造。
线程B调用instance,判定p非空, 将其返回给instance的调用者。调用者对指针解引用以获得singleton,噢,一个还没有被构造出的对象。bug就出现了。
DCLP能够良好的工作仅当步骤一和二在步骤三之前被执行,但是并没有方法在C或C++中表达这种限制。这就像是插在DCLP心脏上的一把匕首:我们需要在相对指令顺序上定义限制,但是我们的语言没有给出表达这种限制的方法。

DCLP问题在C++11中,这个问题得到了解决。
因为新的C++11规定了新的内存模型,保证了执行上述3个步骤的时候不会发生线程切换,相当这个初始化过程是“原子性”的的操作,DCL又可以正确使用了。

该实例的析构函数什么时候执行?

如果在类的析构行为中有必须的操作,比如关闭文件,释放外部资源,那么上面的代码无法实现这个要求。我们需要一种方法,正常的删除该实例。

可以在程序结束时调用GetInstance(),并对返回的指针掉用delete操作。这样做可以实现功能,但不仅很丑陋,而且容易出错。因为这 样的附加代码很容易被忘记,而且也很难保证在delete之后,没有代码再调用GetInstance函数。

一个妥善的方法是让这个类自己知道在合适的时候把自己删除,或者说把删除自己的操作挂在操作系统中的某个合适的点上,使其在恰当的时候被自动执行。

我们知道,程序在结束的时候,系统会自动析构所有的全局变量。事实上,系统也会析构所有的类的静态成员变量,就像这些静态成员也是全局变量一样。利用这个特征,我们可以在单例类中定义一个这样的静态成员变量,而它的唯一工作就是在析构函数中删除单例类的实例。
类CGarbo被定义为Singleton的私有内嵌类,以防该类被在其他地方滥用。

程序运行结束时,系统会调用Singleton的静态成员Garbo的析构函数,该析构函数会删除单例的唯一实例。

使用这种方法释放单例对象有以下特征:
在单例类内部定义专有的嵌套类;
在单例类内定义私有的专门用于释放的静态成员;
利用程序在结束时析构全局变量的特性,选择最终的释放时机;
使用单例的代码不需要任何操作,不必关心对象的释放。

使用静态局部变量

class singleton 
{

private:
	singleton() {}
public:
    singleton *instance();
};

singleton *singleton::instance() 
{
    static singleton p;
    return &p;
}

利用了静态局部变量的特性:
局部静态变量只会在第一次调用该函数时被初始化,且仅能被初始化一次,即:在之后无论再调用多少次该函数,其中的局部变量均不会再初始化。

线程安全情况:
单线程下,正确。
C++11及以后的版本(如C++14)的多线程下,正确。
C++11之前的多线程下,不一定正确。
原因在于在C++11之前的标准中并没有规定local static变量的内存模型。于是乎它就是不是线程安全的了。但是在C++11却是线程安全的,这是因为新的C++标准规定了当一个线程正在初始化一个变量的时候,其他线程必须得等到该初始化完成以后才能访问它。

延伸阅读:
声明为 GetInstance 方法静态变量的单例实例,是否线程安全?

使用pthread_once

如果是在unix平台的话,除了使用atomic operation外,在不适用C++11的情况下,还可以通过pthread_once来实现Singleton。

class singleton {
private:
    singleton(); //私有构造函数,不允许使用者自己生成对象
    singleton(const singleton &other);
    //要写成静态方法的原因:类成员函数隐含传递this指针(第一个参数)
    static void init() 
    {
        p = new singleton();
    }
    static pthread_once_t ponce_;
    static singleton *p; //静态成员变量 
public:
    singleton *instance() 
    {
        // init函数只会执行一次
        pthread_once(&ponce_, &singleton::init);
        return p;
    }
};

饿汉式

饿汉式保证线程安全

class singleton 
{
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
};

// 代码一运行就初始化创建实例 ,本身就线程安全
singleton *singleton::p = new singleton();
singleton* singleton::instance() 
{
    return p;
}