03. 单例模式

一、单例模式概述

  单例模式(Singleton Pattern)属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

  单例模式的要点包括:

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。

  单例模式的主要目的是确保一个类只有一个实例,并提供一个全局访问点来访问该实例。这样可以避免频繁创建和销毁全局使用的类实例,从而节省系统资源。当需要控制实例数目,或者节省系统资源时,可以考虑使用单例模式。

二、C++实现单例模式

单例模式

  由于每次使用 new 关键字来实例化 Singleton 类时都将产生一个新对象,为了确保 Singleton 实例的唯一性,需要禁止类的外部直接使用 new 来创建对象,因此需要将 Singleton 的构造函数的可见性改为 private。

private:
    // 1、构造函数私有化,外部不能通过new创建对象
    Singleton(void);

  将构造函数的可见性改为 private 后,虽然类的外部不能再使用 new 来创建对象,但是在 Singleton 的内部还是可以创建对象的,可见性只对类外有效。因此,可以在 Singleton 中创建并保存这个唯一实例。为了让外界可以访问这个唯一实例,需要在 Singleton 中定义一个静态的 Singleton 类型的私有成员变量 instance。

private:
    // 2、本类内部创建对象实例
    static Singleton * instance;

  为了保证成员变量的封装性,将 Singleton 类型的 instance 对象的可见性设置为 private,我们还需要提供一个公有的静态方法 getInstance(),来供外界该如何使用该成员变量并实例化该成员变量。

  在 getInstance() 方法中首先判断 instance 对象是否存在,如果不存在,则使用 new 关键字创建一个新的 Singleton 类型的 instance 对象,再返回新创建的 instance 对象;否则直接返回已有的 instance 对象。

Singleton * Singleton::getInstance(void)
{
    if (Singleton::instance == NULL)
    {
        instance = new Singleton();
    }
  
    return instance;
}

  其完整代码如下:

class Singleton
{
private:
    // 1、构造函数私有化,外部不能通过new创建对象
    Singleton(void);

    // 2、本类内部创建对象实例
    static Singleton * instance;

public:
    // 3、对外提供访问接口,返回对象实例
    static Singleton * getInstance(void);
};
Singleton * Singleton::instance = NULL;

Singleton::Singleton(void) {}

Singleton * Singleton::getInstance(void)
{
    if (Singleton::instance == NULL)
    {
        instance = new Singleton();
    }
  
    return instance;
}

  main() 函数:

#include <iostream>

int main(void)
{
    Singleton * s1 = Singleton::getInstance();
    Singleton * s2 = Singleton::getInstance();
    Singleton * s3 = s1;

    std::cout << ((s1 == s2) ? "s1 == s2" : "s1 != s2") << std::endl;
    std::cout << ((s1 == s3) ? "s1 == s3" : "s1 != s3") << std::endl;

    return 0;
}

三、饿汉式单例与懒汉式单例

3.1、饿汉式单例

  饿汉式单例 就是 在定义静态变量的时候实例化单例类,在类加载使就已经创建好了单例对象。类被加载时,静态变量 instance 会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。

class Singleton
{
private:
    // 1、构造函数私有化,外部不能通过new创建对象
    Singleton(void);

    // 2、本类内部创建对象实例
    static Singleton * instance;

public:
    // 3、对外提供访问接口,返回对象实例
    static Singleton * getInstance(void);
};
Singleton * Singleton::instance = new Singleton();

Singleton::Singleton(void) {}

Singleton * Singleton::getInstance(void)
{
    if (Singleton::instance == NULL)
    {
        instance = new Singleton();
    }
  
    return instance;
}

3.2、懒汉式单例

  懒汉式单例第一次调用 getInstance() 方法时实例化,在类加载时并不自行实例化,这种技术又称为 延迟加载技术(Lazy Load),即需要的时候再加载实例。懒汉式单例在多线程的情况下要解决线程安全问题。

  假如某一瞬间线程 A 和线程 B 都在调用 getInstance() 方法,此时 instance 对象为 null 值,均能通过 “instance == nullptr” 的判断。由于实现了加锁机制,线程 A 进入同步锁锁定的代码中执行实例创建代码,线程 B 处于排队等待状态,必须等待线程 A 执行完毕后才可以进入同步锁锁定的代码。但当 A 执行完毕时,线程 B 并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想。因此需要进行进一步改进,在同步锁锁定的代码中再进行一次 “instance == nullptr” 判断,这种方式称为 双重检查锁定(Double-Check Locking)。

#include <thread>
#include <mutex>

std::mutex mtx;

class Singleton
{
private:
    // 1、构造函数私有化,外部不能通过new创建对象
    Singleton(void);

    // 2、本类内部创建对象实例
    static Singleton * instance;

public:
    // 3、对外提供访问接口,返回对象实例
    static Singleton * getInstance(void);
};
Singleton * Singleton::instance = nullptr;

Singleton::Singleton(void) {}

Singleton * Singleton::getInstance(void)
{
    // 第一层判断
    if (Singleton::instance == nullptr)
    {
        // 加锁
        mtx.lock();
        // 第二层判断
        if (Singleton::instance == nullptr)
        {
            instance = new Singleton();
        }
        // 解锁
        mtx.unlock();
    }
  
    return instance;
}

3.3、饿汉式单例VS懒汉式单例

  饿汉式单例类 在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。

  懒汉式单例类 在第一次使用时创建,无须一直占用系统资源,实现了延迟加载。但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的概率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。

四、单例模式的总结

4.1、单例模式的优点

  • 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源。对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。基于单例模式,开发人员可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题。(

4.2、单例模式的缺点

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。
  • 现在很多面向对象语言(如 Java、C#)的运行环境都提供了自动垃圾回收技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

4.3、单例模式的适用场景

  • 系统只需要一个实例对象。
  • 客户调用类的单个实例只允许使用一个公共访问点。除了该公共访问点,不能通过其他途径访问该实例。
posted @ 2023-08-20 18:54  星光樱梦  阅读(21)  评论(0编辑  收藏  举报