当前位置: 首页 > article >正文

设计模式中单例模式中懒汉模式的问题

设计模式中单例模式中懒汉模式的问题

今天在项目中遇到了要使用懒汉模式的问题。百度之后,发现还有很多细节是自己之前没有见过的。于是记录一下。下面是在AI助手中的说明。

单例模式的懒汉模式(Lazy Singleton)是在需要时才创建实例,而不是在程序启动时就创建。懒汉模式常见的实现方式存在一些问题,尤其是在多线程环境中。

下面将详细讲解懒汉模式的优缺点及其问题。

1.懒汉模式的实现通常如下:

class Singleton {
private:
    static Singleton* instance;

    // 私有构造函数,防止外部创建实例
    Singleton() {}

public:
    // 获取实例的公共方法
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }

    // 防止拷贝构造和赋值操作
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

// 静态成员初始化
Singleton* Singleton::instance = nullptr;

懒汉模式的问题

  1. 线程安全问题

    在多线程环境中,懒汉模式存在问题。当多个线程同时调用 getInstance() 方法时,可能会导致多个线程同时判断 instance == nullptrtrue,然后都去创建实例,导致多个实例的创建。为了解决这个问题,可以使用互斥锁(mutex)来保证线程安全,但是这样会降低性能。

修改后的线程安全实现(使用 mutex):

#include <mutex>

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx; // 用于保护实例的互斥锁

    Singleton() {}

public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return instance;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

这样可以避免多个线程同时创建多个实例,但性能会受到影响,因为每次获取实例时都需要加锁。在上述实现中,我们使用了双重检查锁定(Double-Checked Locking)模式来提高性能。即第一次检查 instance == nullptr 时不加锁,只有在需要创建实例时才加锁。

2. 使用 std::atomic 确保内存可见性和防止指令重排

C++11 提供了 std::atomic 来确保操作的原子性并避免指令重排。在单例模式中,使用 std::atomic 可以确保对 instance 的写操作对所有线程可见,并且避免因指令重排引发的问题。

为了保证 instance 的初始化过程对所有线程是安全的,我们可以使用 std::atomic 作为 instance 的类型。这样,C++11 保证了原子操作的内存顺序,防止了指令重排问题。

#include <atomic>

class Singleton {
private:
    static std::atomic<Singleton*> instance;

    Singleton() {}

public:
    static Singleton* getInstance() {
        Singleton* temp = instance.load(std::memory_order_acquire);
        if (temp == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            temp = instance.load(std::memory_order_relaxed);
            if (temp == nullptr) {
                temp = new Singleton();
                instance.store(temp, std::memory_order_release);
            }
        }
        return temp;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

std::atomic<Singleton*> Singleton::instance{nullptr};

3.多线程性能开销

在多线程环境下,即使是双重检查锁定,也会引入性能开销。锁的争用可能会导致性能瓶颈,尤其是在实例已经创建时,锁的机制会影响到性能。因此,如果创建的实例较为复杂且开销较大,这种性能问题更加明显。

改进方法

  1. 使用 std::call_once
    C++11 提供了 std::call_once,它可以确保某个代码块在多线程中只会被执行一次。使用它可以避免锁的开销。

    改进后的实现:

    cpp#include <mutex>
    
    class Singleton {
    private:
        static Singleton* instance;
        static std::once_flag flag;  // 标记
    
        Singleton() {}
    
    public:
        static Singleton* getInstance() {
            std::call_once(flag, []() {
                instance = new Singleton();
            });
            return instance;
        }
    
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    };
    
    Singleton* Singleton::instance = nullptr;
    std::once_flag Singleton::flag;
    

    这种方式在多线程中比加锁性能更好。

  2. 使用 static 局部变量
    最简单且性能最优的方法是利用 C++ 的静态局部变量特性。C++11 标准保证了静态局部变量的初始化是线程安全的,且只会初始化一次。这是推荐的懒汉模式实现方式。

    cppclass Singleton {
    private:
        Singleton() {}
    
    public:
        static Singleton* getInstance() {
            static Singleton instance;  // 线程安全的静态局部变量
            return &instance;
        }
    
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    };
    

    这种方式不需要显式加锁,且由编译器保证线程安全,并且只会初始化一次,性能非常好。

总结

懒汉模式在多线程环境下需要特别小心,常见的问题包括线程安全问题、内存泄漏、性能开销等。为了确保线程安全并避免性能问题,推荐使用 std::call_once 或 C++11 中的静态局部变量来实现单例模式。通过这些改进,可以大幅度提升懒汉模式的效率和安全性。


http://www.kler.cn/a/447511.html

相关文章:

  • SAP HCM 考勤时间冲突到分 源码分析
  • kafka常用命令(持续更新)
  • 前端和后端解决跨域问题的方法
  • uniapp新建项目hello,什么都没干提示应用未关联服务空间,请在uniCloud目录右键关联服务空间
  • Mybatis中使用MySql触发器报错:You have an error in your SQL syntax; ‘DELIMITER $$
  • 【Prompt Engineering】7 聊天机器人
  • APM32F411使用IIS外设驱动es8388实现自录自播
  • Mono里运行C#脚本2—参数配置
  • 如何在Java中使用封装好的API接口?
  • 【Leetcode 热题 100】124. 二叉树中的最大路径和
  • 混合开发环境---使用编程AI辅助开发Qt
  • NOI与USACO的关系
  • 博客系统(Java 实现详解)
  • 【最大似然估计】之Python实现
  • 图像生成工具WebUI
  • MySQL知识汇总(二):select
  • ARM原理
  • 文件包含tomato靶机通关
  • 39.在 Vue3 中使用 OpenLayers 导出 GeoJSON 文件及详解 GEOJSON 格式
  • LLMs之rStar:《Mutual Reasoning Makes Smaller LLMs Stronger Problem-Solvers》翻译与解读
  • 前端知识补充—HTML
  • Java每日一题(2)
  • 260-高速AD/DA开发板 大容量FPGA编程 USRP K7-SDR Kintex-7 XC7K325T
  • 基于NodeMCU的物联网空调控制系统设计
  • zookepper安装部署
  • Vue.js 核心概念:模板、指令、数据绑定