欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

一篇就懂设计模式:单例模式

程序员文章站 2022-06-05 19:56:54
...

单例模式(Singleton Pattern)

单例模式顾名思义就是类只有一个实例的模式。当在实际场景中只能存在一个唯一对象时,我们无法保证用户只创建了一个实例对象,尤其是在对系统功能的版本迭代过程中,不同的开发人员可能会创建多个实例对象,这与系统的逻辑可能是不符的。因此需要由类限制实例对象创建的行为。单例模式正是这样一种设计模式,在GOF中其定义如下:

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

那么问题来了,如何在类内限制实例对象的创建?

主要在于两点:(1)构造函数以及类成员对象的访问权限;(2)在创建实例时需要判断是否已经有实例对象创建

基本思路:

  • 要限制用户创建对象的行为,用户不能直接调用构造函数实例化对象,因此构造函数要设置为private;
  • 由于类只能有一个实例,因此在类内定义一个该类类型的成员变量,此外用static关键字来设置全局特性;
  • 提供一个public接口给用户,该接口中会判断是否已经有实例化的对象,若没有则调用构造函数进行对象的实例,由于在创建实例对象时需要对类进行判断,因此不能通过对象调用该接口,因此需要用static修饰;

根据上面的思路,可以得到单例模式的结构图,如下:

一篇就懂设计模式:单例模式

接下来给一段代码样例:

//单例模式之懒汉模式
class Singleton
{
private:
    static Singleton* s_instance;
    
    Singleton();

public:
    static Singleton* GetInstance();
}

Singleton* Singleton::s_instance= nullptr;    //初始化

Singleton::Singleton()
{

}

// 用户创建实例
Singleton* Singleton::GetInstance()
{
    if(nullptr == s_instance)
    {
        s_instance= new Singleton();
    }
}

上面创建的类可以实现单例模式,这种创建方式被称作懒汉模式。但是如果有多个线程同时调用了GetInstance方法来实例化对象,可能会出现一个线程已经实例化对象,而另外的线程没有及时得知,又重新创建了实例,此时就破坏了“单例”这一特性。因此这种单例模式是线程不安全的。

那么要实现线程安全,最容易想到的方法是加锁,利用双重锁机制设置临界区,使得在多线程情况下仍能保证只能创建了一个实例,修改后的代码如下:

//懒汉模式(双检锁)
class Singleton
{
private:
    static Singleton* s_instance;
    
    Singleton();

public:
    static std::mutex s_mutex1;
    static std::mutex s_mutex2;
    static Singleton* GetInstance();
}

Singleton* Singleton::s_instance = nullptr;    //初始化
std::mutex Singleton::s_mutex1;
std::mutex Singleton::s_mutex2;

Singleton::Singleton()
{

}

// 用户创建实例
Singleton* Singleton::GetInstance()
{
    if(nullptr == s_instance)
    {
        std::lock_guard<std::mutex> lock1(s_mutex1);
        if(nullptr == s_instance)
        {
            std::lock_guard<std::mutex> lock2(s_mutex2);
            instance = new Singleton();
        }
    }
}

利用双检锁的懒汉模式可以较好地实现单例模式下的线程安全。此外我们看到懒汉模式实例化对象的特点,只有当需要用到实例时才进行实例化。

除了懒汉模式之外,还有一种单例模式,饿汉模式,代码如下:

//单例模式之饿汉模式
class Singleton
{
private:
    static Singleton* s_instance;
    
    Singleton();

public:
    static Singleton* GetInstance();
}

Singleton* Singleton::s_instance= new Singleton;    //初始化

Singleton::Singleton()
{

}

// 用户访问实例
Singleton* Singleton::GetInstance()
{
    return s_instance;
}

从上面的代码可以看出,饿汉模式中在全局中就进行了实例化,提前占用了系统资源,而GetInstance只是用户访问该实例的一个接口,因此是线程安全的。那么懒汉模式和饿汉模式都能够实现线程安全,在实际开发过程中要如何选择呢?

懒汉模式:只有在需要使用类实例时才进行实例化,因此在访问量较小时使用,以时间换取空间。

饿汉模式:不管是否需要使用类实例,在全局中就先进行实例化,因此在访问量较大,线程较多的情况下使用,以空间换取时间。

最后给出一个可以直接运行的饿汉模式的实例,可以根据此实例进行一个修改

#include <iostream>
#include <string>
//饿汉模式
class SingletonInstance
{
private:
    SingletonInstance() {};

    static SingletonInstance *s_instance;

public:
    static SingletonInstance* getInstance()
    {
        return s_instance;
    }
    
};

SingletonInstance* SingletonInstance::s_instance= new SingletonInstance();

int main()
{
    SingletonInstance *instance1 = SingletonInstance::getInstance();
    SingletonInstance *instance2 = SingletonInstance::getInstance();

    if (instance1 == instance2)
    {
        std::cout << "instance1 & instance2: Same." << std::endl;
    }
    else
    {
        std::cout << "instance1 & instance2: Different." << std::endl;
    }
}