深入浅出设计模式——创建型模式之单例模式 Singleton

发布于:2025-08-02 ⋅ 阅读:(14) ⋅ 点赞:(0)


代码仓库
在这里插入图片描述

“天上天下,唯我独尊”——单例模式

你能在电脑上调出两个Windows任务管理器吗?
假设能,如果两个管理器显示的数据相同,那何必要存在两个呢?
如果两个管理器显示的数据不同,那我该相信哪一个呢?

试试看,应该有且仅有一个吧?一个系统里有且仅有一个Windows任务管理器实例供外界访问 。如何保证系统里有且仅有一个实例对象呢?并且能够供外界访问?你可以在系统里定义一个统一的全局变量,但这并不能防止创建多个对象(想一想,为什么?)这就是单例模式的典型应用。

对于一个软件系统中的某些类来说,只有一个实例很重要。假设Windows系统上可以同时调出两个Windows任务管理器,这两个任务管理器显示的都是同样的信息,那势必造成内存资源的浪费;如果这两个任务管理器显示的是不同的信息,这也给用户带来了困惑,到底哪一个才是真实的状态?

单例模式简介

单例模式定义:
确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

在这里插入图片描述

单例模式结构

单例模式结构非常简单,其UML图如下所示,只包含一个类,即单例类。为防止创建多个对象,其构造函数必须是私有的(外界不能访问)。另一方面,为了提供一个全局访问点来访问该唯一实例,单例类提供了一个公有方法getInstance来返回该实例。

在这里插入图片描述

饿汉式

饿汉式:变量在声明时便初始化。

// 饿汉式(立即加载)
// 饿汉式(Hungry Singleton):程序启动时立即创建对象
class Singleton_Hungry {
public:
    static Singleton_Hungry* getInstance() {
        std::cout << "\n[Hungry] 获取单例实例" << std::endl;
        static Singleton_Hungry instance; // 推荐方法,更安全更现代化
        return &instance;
    }

    void doSomething() const {
        std::cout << "\n[Hungry] 正在执行任务..." << std::endl;
    }
    
private:
    // 禁止外界创建新的实例(私有构造、删除拷贝构造和拷贝赋值)。
    Singleton_Hungry() {
        std::cout << "\n[Hungry] 构造函数调用" << std::endl;
    }

    ~Singleton_Hungry() {
        std::cout << "\n[Hungry] 析构函数调用" << std::endl;
    }

    Singleton_Hungry(const Singleton_Hungry&) = delete;
    Singleton_Hungry& operator=(const Singleton_Hungry&) = delete;

    // static Singleton_Hungry* instance;
};

// 静态成员初始化 ⚠️头文件定义静态变量,错误!
// 根本原因:违反了单一定义规则(One Definition Rule, ODR)
// 你在头文件中定义了一个静态变量,头文件会被多个.cpp文件包含,这就导致在每个.cpp文件中都定义了一遍,最终会导致链接时的冲突,出现重复定义(multiple definition)错误。
// Singleton_Hungry* Singleton_Hungry::instance = new Singleton_Hungry();

可以看到,我们将构造方法定义为 private,这就保证了其他类无法实例化此类,必须通过 getInstance 方法才能获取到唯一的 instance 实例,非常直观。但饿汉式有一个弊端,那就是即使这个单例不需要使用,它也会在类加载之后立即创建出来,占用一块内存,并增加类初始化时间。就好比一个电工在修理灯泡时,先把所有工具拿出来,不管是不是所有的工具都用得上。就像一个饥不择食的饿汉,所以称之为饿汉式。

懒汉式

懒汉式:先声明一个空变量,需要用时才初始化。例如:

我们先声明了一个 instance 变量,当需要使用时判断此变量是否已被初始化,没有初始化的话才 new 一个实例出来。就好比电工在修理灯泡时,开始比较偷懒,什么工具都不拿,当发现需要使用螺丝刀时,才把螺丝刀拿出来。当需要用钳子时,再把钳子拿出来。就像一个不到万不得已不会行动的懒汉,所以称之为懒汉式

懒汉式解决了饿汉式的弊端,好处是按需加载,避免了内存浪费,减少了类初始化时间。

Singleton.h

// 确保一个类只有一个实例。
// 提供全局访问点,让用户方便访问

// 懒汉式(线程安全,推荐写法)
// 懒汉式(Lazy Singleton):延迟创建对象(用时才创建)
class Singleton_Lazy {
public:
    // 第一次 调用时,执行内部的 lambda 表达式,创建一个新的单例对象。
    // 后续所有次 调用时,都直接跳过这个 lambda,不再执行创建对象的操作。
    static Singleton_Lazy* getInstance() {
        // template< class Callable, class... Args >
        // void call_once(std::once_flag& flag, Callable&& f, Args&&... args);
        // 确保给定的**可调用对象(如lambda表达式)**仅被调用一次。
        // 如果多个线程同时执行该行代码,也能确保只有一个线程实际执行了初始化操作,其他线程则会等待,直到第一次调用完成后再继续执行,且不会重复执行初始化操作。
        // flag:一个标志位,标识对应的初始化是否已经完成。
        // Callable:一个可调用对象(比如lambda表达式或函数),代表真正要执行的初始化动作。
        std::call_once(initFlag, []() {
            std::cout << "\n[Lazy] 创建新的实例" << std::endl;
            // instance = std::make_unique<Singleton_Lazy>(); // ✅ 更安全,更推荐
            instance.reset(new Singleton_Lazy());
        });

        std::cout << "\n[Lazy] 获取单例实例" << std::endl;
        return instance.get();
    }

    void doSomething() const {
        std::cout << "\n[Lazy] 正在执行任务..." << std::endl;
    }

private:
    Singleton_Lazy() {
        std::cout << "\n[Lazy] 构造函数调用" << std::endl;
    }

    ~Singleton_Lazy() {
        std::cout << "\n[Lazy] 析构函数调用" << std::endl;
    }

    // 拷贝构造函数(Copy Constructor)
    // 当使用一个已存在对象初始化另一个新对象时,调用拷贝构造函数
    Singleton_Lazy(const Singleton_Lazy&) = delete;

    // 拷贝赋值运算符(Copy Assignment Operator)
    // 当使用一个已存在的对象去给另一个已存在的对象赋值时调用。
    Singleton_Lazy& operator=(const Singleton_Lazy&) = delete;

    // ⚠️一定要保留这两个声明!
    static std::unique_ptr<Singleton_Lazy> instance;
    static std::once_flag initFlag;

    // 🔑加上这一句:
    // 允许 std::unique_ptr 调用私有析构函数
    friend class std::default_delete<Singleton_Lazy>;
};

Singleton.cpp

#include "Singleton.h"

std::unique_ptr<Singleton_Lazy> Singleton_Lazy::instance = nullptr;
std::once_flag Singleton_Lazy::initFlag;

客户端示例

#define THREAD_NUM 6
#include <iostream>
#include <pthread.h>
#include <sys/syscall.h>
#include <unistd.h>
#include "Singleton.h"

pid_t GetThreadId() {
    // syscall 是一个系统调用接口,可以让你直接调用操作系统提供的底层功能。
    // SYS_gettid 是 Linux 系统调用号,表示获取当前线程的线程ID(gettid)。
    // syscall(SYS_gettid) 实际上是执行 gettid() 系统调用的操作,返回当前线程的线程ID。
    // 该调用返回当前线程的线程ID,通常与 pthread_self() 的返回值相同,但是 gettid 是返回内核级线程ID,而 pthread_self() 返回的是 POSIX 线程库级别的线程ID
    // SYS_gettid 是一个常量,表示获取当前线程ID的系统调用号。
    // 每个系统调用都有一个唯一的编号(常量),用于标识该系统调用。SYS_gettid 对应的是获取线程ID的操作。
    return syscall(SYS_gettid);
}

void* callSingleton_Lazy(void* arg) {
    int threadID = *(int*)arg;
    Singleton_Lazy *s = Singleton_Lazy::getInstance();
    printf("[Lazy] 线程编号: %d, 实例地址: %d\n", threadID, GetThreadId());
    // printf("[Hungry] 线程编号: %d, 实例地址: %p\n", threadID, s);
    return 0;
}

void* callSingleton_Hungry(void* arg) {
    // 将arg 从 void* 类型的通用指针强制转换成 int*类型的指针, 然后对转换后的指针解引用,取出实际的整型数值(即线程编号)。
    int threadID = *(int*)arg;
    Singleton_Hungry *s = Singleton_Hungry::getInstance();
    printf("[Hungry] 线程编号: %d, 实例地址: %d\n", threadID, GetThreadId());
    // printf("[Hungry] 线程编号: %d, 实例地址: %p\n", threadID, s);
    return 0;
}

int main() {
    pthread_t threads_pool[THREAD_NUM];
    int tids[THREAD_NUM], params[THREAD_NUM];

    for(int i = 0; i < THREAD_NUM; i++) {
        params[i] = i; // 独立参数,避免竞争

        /*
        int pthread_create(pthread_t *restrict thread,
                          const pthread_attr_t *restrict attr,
                          void *(*start_routine)(void *),
                          void *restrict arg);
        */
        // 前半部分线程调用懒汉式单例
        if(i < THREAD_NUM / 2)
            tids[i] = pthread_create(&threads_pool[i], NULL, callSingleton_Lazy, (void*)&params[i]);
        else // 后半部分线程调用饿汉式单例
            tids[i] = pthread_create(&threads_pool[i], NULL, callSingleton_Hungry, (void*)&params[i]);

        // On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.
        if(tids[i]) {
            printf("Error: unable to create thread.\n");
            exit(-1);
        }
    }

    for(int i = 0; i < THREAD_NUM; i++) {
        // On success, pthread_join() returns 0; on error, it returns an error number
        tids[i] = pthread_join(threads_pool[i], NULL);
        if(tids[i]) {
            printf("Error: unable to join thread.\n");
            exit(-1);
        }
    }
    printf("main exiting.\n");
    return 0;
}

运行结果

在这里插入图片描述

单例模式总结

单例模式让一个类同时负责了『业务功能』和『自身的创建与生命周期管理』两个职责。
在这里插入图片描述
在这里插入图片描述

构建型模式 Creational Patterns 小结 Summary

在这里插入图片描述

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!


网站公告

今日签到

点亮在社区的每一天
去签到