2023-6-12-第三式单例模式

简介: 2023-6-12-第三式单例模式

😉一、基础概念

单例模式(Singleton Pattern)是一种创建型设计模式,它保证某一个类只有一个实例存在,并提供一个全局访问点。

在单例模式中,通过将类的构造函数设为私有,从而防止外部直接创建对象。同时,通过一个静态方法或者变量来获取该类的唯一实例。如果该类的实例不存在,则创建一个新的实例并返回;如果该类的实例已经存在,则直接返回该实例。

单例模式的优点是可以避免因为创建多个实例而导致的资源浪费和性能下降。同时,由于该类只有一个实例存在,因此可以更好地控制该实例的状态和行为。此外,单例模式还可以提供一个全局访问点,使得其他对象可以方便地访问该实例。

需要注意的是,单例模式虽然可以解决某些问题,但也可能会引入新的问题。例如,单例模式可能会导致代码的耦合性增加,使得代码难以测试和维护。因此,在使用单例模式时需要权衡利弊,合理使用。


🐱‍🐉二、单例模式实现

懒汉模式

class Singleton {
public:
    static Singleton* getInstance() {
        static Singleton instance;
        return &instance;
    }
    void doSomething() {
        std::cout << "Singleton doSomething" << std::endl;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};
int main() {
    Singleton* instance1 = Singleton::getInstance();
    Singleton* instance2 = Singleton::getInstance();
    if (instance1 == instance2) {
        std::cout << "instance1 and instance2 are the same instance" << std::endl;
    } else {
        std::cout << "instance1 and instance2 are different instances" << std::endl;
    }
    instance1->doSomething();
    instance2->doSomething();
    return 0;
}

在这个示例中,Singleton 类的构造函数是私有的,因此外部无法直接创建该类的实例。getInstance() 方法是该类的静态方法,用于获取该类的唯一实例。在 getInstance() 方法中,使用了静态局部变量的方式来创建该类的唯一实例。由于静态局部变量只会在第一次调用时初始化,因此可以保证该类只有一个实例存在。

需要注意的是,在多线程环境下,需要考虑线程安全问题。可以使用加锁的方式来解决该问题。此外,在使用单例模式时,还需要避免使用全局变量或静态变量等方式来创建对象,这样会破坏单例模式的原则。

饿汉模式

class Singleton {
public:
    static Singleton* getInstance() {
        return &instance;
    }
    void doSomething() {
        std::cout << "Singleton doSomething" << std::endl;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    static Singleton instance;
};
Singleton Singleton::instance;
int main() {
    Singleton* instance1 = Singleton::getInstance();
    Singleton* instance2 = Singleton::getInstance();
    if (instance1 == instance2) {
        std::cout << "instance1 and instance2 are the same instance" << std::endl;
    } else {
        std::cout << "instance1 and instance2 are different instances" << std::endl;
    }
    instance1->doSomething();
    instance2->doSomething();
    return 0;
}

在这个示例中,Singleton 类的唯一实例是在类定义中直接创建的,因此可以保证在程序启动时就已经创建了该实例,即所谓的“饿汉模式”。需要注意的是,由于该实例是在程序启动时就创建的,因此可能会影响程序启动的速度和内存占用。

在使用饿汉单例模式时,需要注意线程安全问题。如果该类的实例需要在多线程环境下使用,需要使用加锁的方式来保证线程安全。


🎉三、单例模式与线程安全

单例模式的懒汉模式和饿汉模式都可能会造成线程安全问题,具体原因如下:

  1. 懒汉模式

懒汉模式是指在第一次使用时才创建单例对象。在多线程环境下,如果多个线程同时调用 getInstance() 方法,那么可能会创建多个实例,违反了单例模式的原则。例如,当一个线程在判断实例是否存在时,还没有创建实例,此时另一个线程也进入了判断实例是否存在的代码块,也未创建实例,然后两个线程都创建了实例,导致单例模式失效。

  1. 饿汉模式

饿汉模式是指在程序启动时就创建单例对象。在多线程环境下,如果多个线程同时调用 getInstance() 方法,那么可能会访问到未完全初始化的实例,从而导致程序异常或者崩溃。例如,当一个线程正在初始化实例时,另一个线程也调用了 getInstance() 方法,此时可能会访问到未完全初始化的实例。

因此,在实际使用中,需要考虑单例模式的线程安全问题。可以使用加锁的方式来保证线程安全,或者采用其他线程安全的方式来实现单例模式。


🐱‍🚀四、懒汉模式怎么保证线程安全

如果要求在懒汉模式下保证线程安全,并且实例化之后不能再加锁,可以使用双重检查锁定(Double-Checked Locking)的方式来实现。双重检查锁定是一种常用的单例模式实现方式,它可以在保证线程安全的同时,避免了每次调用 getInstance() 方法都需要加锁的性能问题。

具体来说,双重检查锁定的实现方式如下:

class Singleton {
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mutex);
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return instance;
    }
    void doSomething() {
        std::cout << "Singleton doSomething" << std::endl;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    static Singleton* instance;
    static std::mutex mutex;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
int main() {
    Singleton* instance1 = Singleton::getInstance();
    Singleton* instance2 = Singleton::getInstance();
    if (instance1 == instance2) {
        std::cout << "instance1 and instance2 are the same instance" << std::endl;
    } else {
        std::cout << "instance1 and instance2 are different instances" << std::endl;
    }
    instance1->doSomething();
    instance2->doSomething();
    return 0;
}

在这个示例中,getInstance() 方法首先判断实例是否已经存在,如果不存在,则获取互斥锁,然后再次判断实例是否已经存在。由于在获取互斥锁之前已经进行了一次判断,因此可以避免多个线程同时获取互斥锁的情况,从而提高了程序的性能。

需要注意的是,在使用双重检查锁定时,需要注意内存可见性问题。由于编译器和处理器的优化,可能会导致变量的值在不同的线程中不一致,从而导致程序出现异常。可以使用 std::atomic 类型来解决这个问题,或者使用 C++11 中引入的跨平台内存屏障(Memory Barrier)来保证内存可见性。


🎂五、饿汉模式怎么保证线程安全

饿汉模式是指在程序启动时就创建单例对象。在这种情况下,由于实例已经被创建,因此不存在多个线程同时创建实例的问题。所以,饿汉模式本身就是线程安全的。

但是,如果在程序启动时需要进行大量的初始化工作,可能会影响程序的启动速度。因此,可以考虑将初始化工作延迟到实例被第一次使用时再进行,这样可以避免不必要的初始化工作,提高程序的启动速度。

如果需要在饿汉模式下延迟初始化工作,可以使用静态成员变量和静态成员函数的方式来实现。静态成员变量只会在第一次使用时被初始化,因此可以实现延迟初始化的效果。示例代码如下:

class Singleton {
public:
    static Singleton* getInstance() {
        return &getInstanceImpl();
    }
    void doSomething() {
        std::cout << "Singleton doSomething" << std::endl;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    static Singleton& getInstanceImpl() {
        static Singleton instance;
        return instance;
    }
};
int main() {
    Singleton* instance1 = Singleton::getInstance();
    Singleton* instance2 = Singleton::getInstance();
    if (instance1 == instance2) {
        std::cout << "instance1 and instance2 are the same instance" << std::endl;
    } else {
        std::cout << "instance1 and instance2 are different instances" << std::endl;
    }
    instance1->doSomething();
    instance2->doSomething();
    return 0;
}

在这个示例中,getInstanceImpl() 方法返回的是一个静态局部变量 instance 的引用。由于静态局部变量只会在第一次使用时被初始化,因此可以实现延迟初始化的效果。在 getInstance() 方法中,直接返回 getInstanceImpl() 方法的返回值即可。

需要注意的是,在使用静态局部变量时,需要考虑线程安全问题。在多线程环境下,可能会出现多个线程同时访问静态局部变量的情况,从而导致程序出现异常。可以使用 C++11 中引入的线程安全的局部静态变量(Thread-safe Local Static)来解决这个问题。


🥩六、注意事项

单例模式是一种常用的设计模式,但是在使用时需要注意以下几点:

  1. 线程安全性:单例模式在多线程环境下需要保证线程安全性,否则可能会导致多个线程同时创建实例的问题。可以使用懒汉模式、饿汉模式、双重检查锁定等方式来保证线程安全性。
  2. 内存泄漏:单例模式在程序运行期间只会创建一个实例,如果实例没有被正确释放,可能会导致内存泄漏问题。可以使用智能指针等方式来避免内存泄漏问题。
  3. 可维护性:单例模式可能会导致代码的可维护性下降,因为单例模式隐藏了对象的创建和销毁过程,使得代码的调试和修改变得困难。因此,在使用单例模式时需要注意代码的可维护性。
  4. 应用场景:单例模式适用于需要全局唯一实例的场景,例如配置文件、日志系统等。但是,在一些场景下,单例模式可能会导致代码的复杂性增加,因此需要根据实际情况来选择是否使用单例模式。
  5. 可测试性:单例模式可能会导致代码的可测试性下降,因为单例模式隐藏了对象的创建和销毁过程,使得代码的测试变得困难。因此,在使用单例模式时需要注意代码的可测试性。

🍳参考文献

🧊文章总结

提示:这里对文章进行总结:

  本文讲了单例模式的一些注意事项,希望大家学习后有所收获。


目录
相关文章
|
数据采集 监控 API
淘宝淘口令 API 接口全攻略
### 淘口令 API 及相关服务简介 **一、淘口令 API(item_password)** - **功能**:将淘口令转换为商品链接或获取商品信息,支持生成自定义淘口令。 - **申请流程**:注册账号、创建应用、获取凭证、申请权限。 - **调用示例(Python)**:通过签名和请求参数调用接口,生成淘口令。 **二、第三方 API 服务** - **适用场景**:简化开发流程,支持高佣转链、淘口令解析等功能。 - **推荐接口**:万能淘口令生成、淘口令解析真实 URL。
|
数据采集 JSON API
小红书笔记详情 API 接口(小红书 API 系列)
小红书作为热门生活方式平台,拥有海量用户生成内容。通过其笔记详情接口,开发者可获取指定笔记的完整内容、作者信息及互动数据(点赞、评论、收藏数等),助力内容分析与市场调研。接口采用HTTP GET请求,需提供笔记ID,响应数据为JSON格式。注意小红书有严格反爬虫机制,建议使用代理IP并控制请求频率。
2406 3
|
消息中间件 存储 安全
如何理解符号引用和直接引用?
如何理解符号引用和直接引用?
270 11
如何理解符号引用和直接引用?
|
存储 算法 缓存
高并发架构设计三大利器:缓存、限流和降级问题之滑动窗口算法的原理是什么
高并发架构设计三大利器:缓存、限流和降级问题之滑动窗口算法的原理是什么
273 0
|
存储 缓存 监控
一文看懂分布式链路监控系统
本文通过阿里的Eagleeye(鹰眼)和开源的Skywalking,从数据模型、数据埋点以及数据存储三个方面介绍分布式链路监控系统的实现细节,其中将重点介绍Skywalking字节码增强的实现方案。
2757 21
一文看懂分布式链路监控系统
|
开发工具 git C++
通过VS2019 + Git 上传本地代码至云效代码管理 Codeup
通过VS2019 + Git 上传本地代码至云效代码管理 Codeup
2391 0
通过VS2019 + Git 上传本地代码至云效代码管理 Codeup
|
存储 缓存 运维
阿里巴巴鹰眼系统简介
微服务框架带来的好处十分多,比如说它提高了开发的效率,它具备更好的扩展性。可是微服务其实是一把双刃剑,微服务同时也带来了一些问题,比如:
5909 0
阿里巴巴鹰眼系统简介
|
运维 调度 双11
链路追踪(Tracing)其实很简单——分布式链路追踪的起源
作者:夏明(涯海) 创作日期:2022-07-14 专栏地址:【稳定大于一切】【稳定大于一切】冬日的周末,你躺在温暖的被窝里,点了一份可口的外卖;双11的零点,看着满满的购物车,你在疯狂提交订单;假期约上小伙伴,一起激情开黑,五杀超神……在这个精彩纷呈的互联网世界里,你的屏幕背后又隐藏着什么?你的每...
424 0
链路追踪(Tracing)其实很简单——分布式链路追踪的起源
|
自然语言处理 算法 Java
深扒Sentinel背后的实现原理之后,我终于明白它为什么这么强了
最近我在整理代码仓库的时候突然发现了被尘封了接近两年之久的Sentinel源码库 两年前我出于好奇心扒了一下Sentinel的源码,但是由于Sentinel本身源码并不复杂,在简单扒了扒之后几乎就再没扒过了 那么既然现在又让我看到了,所以我准备再来好好地扒一扒,然后顺带写篇文章来总结一下。
|
监控 前端开发 Java
Rpc 调用监控 | 学习笔记
快速学习 Rpc 调用监控
Rpc 调用监控 | 学习笔记

热门文章

最新文章