设计模式之单例模式

概念

单例模式 是一种创建型设计模式1,它保证一个类在整个系统运行期间只有一个实例,并且提供一个全局访问点来访问这个唯一实例。

无论在系统的任何地方、任何时间,对该类进行实例化操作,获取到的都是同一个对象实例。这就像在一个公司中,通常会有一个唯一的总经理,无论从哪个部门去获取总经理这个角色的实例,得到的都是同一个人。

实现原理

实现方式

一般通过将类的构造函数设置为私有,防止外部代码通过常规的new操作符来创建多个实例。只有类自身内部可以创建实例,并且会在类的内部保存这个唯一的实例,确保不会再创建其他实例。

全局访问点

单例模式提供了一个全局访问点,使得系统中的任何其他模块或类都可以方便地访问到这个唯一的实例。就好比在一个城市中,有一个唯一的市政服务中心,城市中的各个机构、企业和居民都可以通过特定的渠道(比如地址、电话等)来访问这个市政服务中心,获取所需的服务或信息。在软件系统中,这个全局访问点通常是一个静态方法属性,通过类名就可以直接调用,方便了不同模块之间对单例对象的共享和使用。

实现类型

饿汉式

原理:在类加载时候就立即创建单例实例,不管是否需要使用该实例,该模式是线程安全2的。

示例代码

#include 

class Singleton {
private:
    // 私有静态实例,在类加载时就创建
    static Singleton* instance;
    // 私有构造函数,防止外部实例化
    Singleton() {}
    // 拷贝构造函数和赋值运算符重载设为私有,防止拷贝和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    // 公共的静态方法,用于获取单例实例
    static Singleton* getInstance() {
        return instance;
    }
    void showMessage() {
        std::cout << "This is a singleton instance." << std::endl;
    }
};

// 静态成员变量需要在类外初始化
Singleton* Singleton::instance = new Singleton();

int main() {
    Singleton* singleton1 = Singleton::getInstance();
    Singleton* singleton2 = Singleton::getInstance();

    if (singleton1 == singleton2) {
        std::cout << "Both pointers point to the same instance." << std::endl;
    }
    singleton1->showMessage();

    return 0;
}

代码解释

  • instance是一个私有静态指针,在类加载时就被初始化为一个Singleton对象的地址。
  • 构造函数被声明为私有,防止外部通过new创建新的实例。
  • 拷贝构造函数和赋值运算符重载被删除,防止对象的拷贝和赋值。
  • getInstance方法返回存储的单例实例。

饱汉式(懒汉式)

原理:在第一次需要使用单例实例时才创建,延迟了实例化的时间,该模式是非线程安全的。

示例代码

#include 

class Singleton {
private:
    // 私有静态实例,初始化为null
    static Singleton* instance;
    // 私有构造函数,防止外部实例化
    Singleton() {}
    // 拷贝构造函数和赋值运算符重载设为私有,防止拷贝和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    // 公共的静态方法,用于获取单例实例
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
    void showMessage() {
        std::cout << "This is a singleton instance." << std::endl;
    }
};

// 静态成员变量初始化为null
Singleton* Singleton::instance = nullptr;

int main() {
    Singleton* singleton1 = Singleton::getInstance();
    Singleton* singleton2 = Singleton::getInstance();

    if (singleton1 == singleton2) {
        std::cout << "Both pointers point to the same instance." << std::endl;
    }
    singleton1->showMessage();

    return 0;
}

代码解释

  • instance初始化为nullptr
  • getInstance方法在第一次调用时检查instance是否为nullptr,如果是则创建新实例。
解决线程安全问题

为了解决懒汉式单例模式在多线程环境下的问题,可以使用互斥锁来保证线程安全。

示例代码

#include 
#include 

class Singleton {
private:
    // 私有静态实例,初始化为null
    static Singleton* instance;
    // 互斥锁,用于线程同步
    static std::mutex mutex_;
    // 私有构造函数,防止外部实例化
    Singleton() {}
    // 拷贝构造函数和赋值运算符重载设为私有,防止拷贝和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    // 公共的静态方法,用于获取单例实例
    static Singleton* getInstance() {
        std::lock_guard lock(mutex_);
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
    void showMessage() {
        std::cout << "This is a singleton instance." << std::endl;
    }
};

// 静态成员变量初始化为null
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex_;

int main() {
    Singleton* singleton1 = Singleton::getInstance();
    Singleton* singleton2 = Singleton::getInstance();

    if (singleton1 == singleton2) {
        std::cout << "Both pointers point to the same instance." << std::endl;
    }
    singleton1->showMessage();

    return 0;
}

代码解释

  • 使用std::mutex作为互斥锁,确保在多线程环境下只有一个线程可以创建实例。
  • std::lock_guard用于自动管理锁的生命周期,在进入getInstance方法时加锁,离开时自动解锁。

Meyers 单例模式

原理:Meyers 单例模式是由 Scott Meyers 提出的一种单例模式实现方式,它利用了 C++ 静态局部变量的特性。在 C++11 及以后的标准中,静态局部变量的初始化是线程安全的,即当多个线程同时首次调用包含静态局部变量的函数时,会保证静态局部变量只被初始化一次。

示例代码

#include 

class Singleton {
private:
    // 私有构造函数,防止外部实例化
    Singleton() {}
    // 拷贝构造函数和赋值运算符重载设为私有,防止拷贝和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    // 公共的静态方法,用于获取单例实例
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
    void showMessage() {
        std::cout << "This is a singleton instance." << std::endl;
    }
};

int main() {
    Singleton& singleton1 = Singleton::getInstance();
    Singleton& singleton2 = Singleton::getInstance();

    if (&singleton1 == &singleton2) {
        std::cout << "Both references refer to the same instance." << std::endl;
    }
    singleton1.showMessage();

    return 0;
}

代码解释

  • 私有构造函数:将构造函数声明为私有,这样外部代码无法通过new操作符来创建Singleton类的实例。
  • 禁止拷贝和赋值:将拷贝构造函数和赋值运算符重载声明为delete,防止通过拷贝或赋值操作创建新的实例。
  • **getInstance**方法:该方法返回一个Singleton类的引用。在方法内部,使用static关键字声明了一个局部变量instance,这个变量会在第一次调用getInstance方法时进行初始化,并且只会初始化一次。之后每次调用getInstance方法都会返回这个已经初始化好的实例的引用。
Meyers 单例模式与饿汉模式、懒汉模式的差异
实例创建时机
  • 饿汉模式:在类加载时就立即创建单例实例。这意味着无论程序是否会用到这个单例对象,它都会在程序启动阶段被创建出来。
  • 懒汉模式:在第一次调用getInstance方法时才创建单例实例,实现了延迟加载。这样可以避免在程序启动时创建不必要的对象,节省系统资源。但在多线程环境下,如果不进行同步处理,可能会出现多个线程同时创建实例的问题。
  • Meyers 单例模式:也是在第一次调用getInstance方法时创建实例,同样实现了延迟加载。不过它利用了 C++ 静态局部变量的特性,保证了线程安全的初始化。
线程安全性
  • 饿汉模式:由于实例在类加载时就已经创建,不存在多线程同时创建实例的问题,因此天生就是线程安全的。
  • 懒汉模式(非线程安全):在多线程环境下,如果多个线程同时调用getInstance方法,并且此时实例还未创建,可能会导致多个线程都判断instancenullptr,从而创建多个实例,破坏了单例的唯一性。为了保证线程安全,需要使用锁机制进行同步,但这会带来一定的性能开销。
  • Meyers 单例模式:在 C++11 及以后的标准中,静态局部变量的初始化是线程安全的,不需要额外的同步机制,避免了锁带来的性能开销,同时也保证了单例的唯一性。
资源管理
  • 饿汉模式:由于实例在程序启动时就被创建,可能会占用一定的系统资源,即使在整个程序运行过程中都不会用到这个实例。
  • 懒汉模式:延迟加载的特性使得只有在真正需要使用实例时才会创建,避免了不必要的资源浪费。
  • Meyers 单例模式:同样实现了延迟加载,在资源管理方面与懒汉模式类似,只有在第一次调用getInstance方法时才会创建实例,提高了资源的利用率。

应用场景

整体应用场景

资源管理类

  • 数据库连接池
    • 在一个应用程序中,频繁地创建和销毁数据库连接会带来较大的性能开销。使用单例模式创建一个数据库连接池,可以确保整个应用程序中只有一个连接池实例。所有的数据库操作都从这个连接池中获取连接,使用完毕后再归还到连接池,这样可以提高数据库连接的复用性,减少资源的浪费。例如,在一个 Web 应用程序中,多个请求可能需要访问数据库,通过单例的数据库连接池,这些请求可以共享连接资源,避免了重复创建连接的开销。
  • 文件系统管理
    • 当应用程序需要对文件系统进行操作时,如文件的读写、目录的创建和删除等,可以使用单例模式来管理文件系统资源。一个单例的文件系统管理类可以统一处理文件操作,避免多个实例同时对同一文件或目录进行操作而导致的冲突和数据不一致问题。比如,在一个日志记录系统中,单例的文件系统管理类可以负责将日志信息写入到指定的日志文件中,确保日志文件的操作是线程安全的。

配置管理类

  • 系统配置信息
    • 应用程序通常会有一些全局的配置信息,如数据库连接参数、服务器地址、缓存大小等。使用单例模式创建一个配置管理类,可以确保在整个应用程序中只有一个地方存储和管理这些配置信息。各个模块可以通过单例的配置管理类来获取所需的配置参数,保证配置信息的一致性。例如,一个企业级的 Java 应用程序可能会使用单例的ConfigManager类来管理所有的配置信息,不同的业务模块可以通过调用ConfigManager.getInstance().getConfig(key)方法来获取相应的配置项。
  • 用户偏好设置
    • 在一些桌面应用程序或移动应用程序中,用户可以设置自己的偏好,如界面主题、字体大小、通知方式等。使用单例模式来管理用户偏好设置,可以确保在应用程序的不同界面和功能模块中都能获取到一致的用户偏好信息。比如,一个音乐播放器应用程序可以使用单例的UserPreferences类来管理用户的播放设置、收藏列表等信息,当用户在设置界面修改了偏好后,其他界面可以立即获取到最新的设置。

日志记录类

  • 全局日志记录
    • 在应用程序的开发和运行过程中,需要记录各种信息,如系统状态、错误信息、用户操作等。使用单例模式创建一个日志记录类,可以确保所有的日志信息都被记录到同一个日志文件或日志系统中,方便对日志进行统一的管理和分析。例如,在一个大型的分布式系统中,各个服务节点可以通过单例的日志记录类将日志信息发送到集中的日志服务器,便于管理员进行监控和排查问题。
  • 日志级别管理
    • 单例的日志记录类还可以方便地管理日志的级别,如调试信息、警告信息、错误信息等。通过单例模式,可以在一个地方统一设置和修改日志级别,而不需要在每个日志记录的地方进行单独的设置。比如,在一个 Java 应用程序中,可以使用单例的Logger类,通过调用Logger.getInstance().setLevel(Level.DEBUG)方法来设置全局的日志级别。

系统监控类

  • 性能监控
    • 对应用程序的性能进行监控是保证系统稳定运行的重要手段。使用单例模式创建一个性能监控类,可以实时收集和统计应用程序的各项性能指标,如 CPU 使用率、内存占用、响应时间等。由于只需要一个实例来进行监控,避免了多个监控实例之间的数据冲突和重复统计。例如,在一个高并发的 Web 服务器中,单例的性能监控类可以定期收集服务器的性能数据,并将数据发送到监控系统进行分析和展示。
  • 系统状态监控
    • 除了性能监控,还可以使用单例模式来监控系统的状态,如服务的可用性、数据库的连接状态等。当系统状态发生变化时,单例的监控类可以及时发出警报或进行相应的处理。比如,在一个微服务架构的应用程序中,单例的系统状态监控类可以实时监测各个微服务的运行状态,当某个服务出现故障时,及时通知运维人员进行处理。

细分应用场景

饿汉式

饿汉式单例模式在类加载时就创建实例,其特点是线程安全,无需额外的同步机制,但可能会造成资源的提前占用。

  • 系统配置加载
    • 在一些对启动速度要求不高,但对配置加载的稳定性和及时性要求较高的系统中,饿汉式单例模式非常适用。例如,在一个嵌入式系统中,系统启动后需要立即加载一系列的硬件配置参数、通信协议配置等。由于这些配置信息在系统运行过程中是固定不变的,并且在系统启动时就必须准备好,使用饿汉式单例模式可以确保配置类在系统启动时就完成实例化,后续的模块可以直接使用该实例获取配置信息,避免了在运行过程中因配置未加载而出现的错误。
  • 全局资源初始化
    • 对于一些需要在系统启动时就完成初始化的全局资源,如数据库连接、缓存服务器连接等,饿汉式单例模式是一个不错的选择。以数据库连接为例,在企业级的 Java 应用程序中,应用启动时需要连接数据库进行数据的读写操作。使用饿汉式单例模式创建数据库连接类,在类加载时就建立好数据库连接,后续的业务逻辑可以直接使用这个连接进行数据操作,减少了每次使用数据库时建立连接的时间开销,提高了系统的响应速度。

懒汉式

懒汉式单例模式在第一次使用时才创建实例,实现了延迟加载,避免了不必要的资源浪费,但在多线程环境下需要额外的同步机制来保证线程安全。

  • 资源密集型对象管理
    • 当系统中存在一些资源密集型的对象,如大型的内存缓存、复杂的算法模型等,这些对象的创建和初始化需要消耗大量的系统资源。在这种情况下,使用懒汉式单例模式可以等到真正需要使用这些对象时才进行创建,避免了在系统启动时就占用大量的资源,提高了系统的资源利用率。例如,在一个图像识别系统中,图像识别模型的加载需要占用大量的内存和计算资源,使用懒汉式单例模式,只有当有图像需要识别时才加载模型,减少了系统的内存压力。
  • 按需创建的服务类
    • 对于一些根据用户需求动态创建的服务类,懒汉式单例模式可以很好地满足这种按需创建的需求。例如,在一个在线游戏服务器中,某些特定的游戏模式可能需要特定的游戏服务来支持,如排行榜服务、活动奖励服务等。这些服务类可以使用懒汉式单例模式实现,只有当玩家触发相应的游戏模式时,才创建对应的服务实例,避免了在服务器启动时创建大量可能不会被使用的服务实例,提高了服务器的性能和灵活性。

Meyers 单例模式

Meyers 单例模式结合了懒汉式的延迟加载和线程安全的特性,利用 C++ 静态局部变量的初始化机制,无需额外的同步操作,实现简单且性能较高。

  • 多线程环境下的单例需求
    • 在多线程的 C++ 应用程序中,当需要一个线程安全的单例对象时,Meyers 单例模式是一个理想的选择。例如,在一个多线程的网络服务器中,需要一个单例的日志记录器来记录服务器的运行状态和用户请求信息。使用 Meyers 单例模式可以确保在多个线程同时访问日志记录器时,只有一个实例被创建,并且不会出现线程安全问题。由于其基于 C++ 标准的线程安全初始化机制,避免了使用锁带来的性能开销,提高了日志记录的效率。
  • 跨模块的单例对象共享
    • 在一个大型的 C++ 项目中,可能会包含多个模块,这些模块之间需要共享一个单例对象。Meyers 单例模式可以方便地实现跨模块的单例对象共享。例如,在一个游戏引擎中,有渲染模块、物理模拟模块、音频模块等,这些模块可能都需要访问一个单例的资源管理器来获取游戏资源。使用 Meyers 单例模式,各个模块可以在需要时通过调用资源管理器的getInstance方法来获取唯一的实例,确保了资源管理器在整个游戏引擎中的唯一性和线程安全性。

  1. 创建型设计模式主要用于处理对象的创建过程,包括对象的实例化、对象的创建方式以及对象创建的时机等。常见的创建型设计模式有:单例模式、工厂模式、建造者模式、圆形模式等。 ↩︎

  2. 在多线程环境中,多个线程可能会同时访问和修改共享资源(如全局变量、静态变量、堆上分配的对象等)。由于线程的执行顺序是不确定的,可能会导致以下几种情况,从而引发线程安全问题:
    竞态条件:多个线程对共享资源进行读写操作时,由于执行顺序的不确定性,最终的结果依赖于线程的执行顺序,这种情况称为竞态条件。例如,两个线程同时对一个计数器变量进行加 1 操作,由于线程调度的原因,可能会导致计数器的值只增加了 1 而不是 2。
    数据不一致:当一个线程正在修改共享资源时,另一个线程可能会读取到修改过程中的中间状态,从而导致数据不一致。比如,一个线程正在更新一个数组的元素,而另一个线程同时读取这个数组,可能会读取到部分更新的数组。 ↩︎

你可能感兴趣的:(设计模式,设计模式,单例模式,c++)