Featured image of post 单例模式

单例模式

单例设计模式的两种方法

引言

单例设计模式(Singleton Design Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这种模式常用于需要控制资源访问、配置管理或共享资源等场景。

本文主要介绍单例设计模式的两种实现方式及其特点。

懒汉模式

  • 懒汉模式创建单例对象,是在需要用到该对象的时候才会去创建单例对象

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    class Singleton {
    public:
        static Singleton* getInstance(){		//需要用到的时候,调用getInstance创建单例对象
            if (nullptr == m_pSingleton) {
                m_pSingleton = new Singleton();
            }
            return m_pSingleton;
        }
    private:
        static Singleton* m_pSingleton;
    
        Singleton() {std::cout << "create instance" << std::endl;}
        ~Singleton() {std::cout << "destroy instance" << std::endl;}
    
        // 删除拷贝构造函数和赋值运算符,确保单例唯一性
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    };
    
    Singleton* Singleton::m_pSingleton = nullptr;
    

懒汉模式在多线程环境下,存在线程不安全性,当多个线程都同时调用getInstance时,如果此时并没有创建单例对象出来,那么多个线程可能同时走到nullptr == m_pSingleton当中去,这时候多个线程同时new单例对象,导致了不安全行为。

  • 解决线程安全可以通过加锁来解决
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Singleton {
public:
    static Singleton* getInstance() {
        std::lock_guard<std::mutex> lock(mutex); // 加锁
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
    // 删除拷贝构造函数和赋值运算符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() {std::cout << "create instance" << std::endl;}
    ~Singleton() {std::cout << "destroy instance" << std::endl;}

    static Singleton* instance;
    static std::mutex mutex;
};

// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
  • 下面还有一种双重检查的方式,这种方式看似更加麻烦,但是也有设计巧妙的地方,读者可以好好思考一下为什么。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Singleton {
public:
    static Singleton* getInstance() {
        if (instance == nullptr) { // 第一次检查
            std::lock_guard<std::mutex> lock(mutex); // 加锁
            if (instance == nullptr) { // 第二次检查
                instance = new Singleton();
            }
        }
        return instance;
    }

    // 删除拷贝构造函数和赋值运算符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() {std::cout << "create instance" << std::endl;}
    ~Singleton() {std::cout << "destroy instance" << std::endl;}

    static Singleton* instance;
    static std::mutex mutex;
};

// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
  • 上面这种模式为什么要检查两次呢,这不是更加麻烦吗?其实里面也有巧妙的设计。

    • 如果按照只检查一次的代码,那么无论什么时候去调用getInstance()都会发生加锁动作。
    • 但是双重检查版本只有在单例对象没有创建的时候才会发生加锁,当对象已经被创建之后,第一次检查nullptr == instance就会跳过加锁过程,直接返回对象。这样会大大减少加锁的开销。
  • 2025-4-13更新

  • 上面那种模式还会涉及一些内存顺序的问题,instance = new Singleton(); 会被分为三个步骤,分配内存,构造,将地址传给instance。但是有时候指令重排会将三个步骤重排为分配内存,将地址传给instance,构造,这时候当两个线程按下面流程进行getInstance()就会发生问题。

    • 线程A 线程B
      if (instance == nullptr)
      std::lock_guard<std::mutex> lock(mutex); // 加锁
      if (instance == nullptr) { // 第二次检查
      分配内存
      将地址给instance (instance不再是nullptr了)
      if (instance == nullptr)
      return instance
      开始使用instance(???instance好像还没有构造)
      构造instance
  • 解决办法就是使用c++当中的原子操作(atomic的操作可以指定一些内存顺序,内存顺序和atomic的内容以后再更新,这里主要强调单例模式的实现)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <atomic>

class Singleton {
public:
    static Singleton* getInstance() {
        if (instance.load() == nullptr) {
            std::lock_guard<std::mutex> lock(mutex);
            if (instance.load() == nullptr) {
                instance.store(new Singleton()); 
            }
        }
        return tmp;
    }

    // 删除拷贝构造函数和赋值运算符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() { std::cout << "create instance" << std::endl; }
    ~Singleton() { std::cout << "destroy instance" << std::endl; }

    static std::atomic<Singleton*> instance;  // 改为 atomic
    static std::mutex mutex;
};

// 初始化
std::atomic<Singleton*> Singleton::instance(nullptr);
std::mutex Singleton::mutex;

饿汉模式

  • 在定义对象的时候,就会创建单例对象出来,这个对象会存在程序的整个作用域。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    class Singleton {
    public:
        static Singleton* getInstance(){
            if (nullptr == m_pSingleton) {
                m_pSingleton = new Singleton();
            }
            return m_pSingleton;
        }
    private:
        int data = 0;
        static Singleton* m_pSingleton;
    
        Singleton() {std::cout << "create instance" << std::endl;}
        ~Singleton() {std::cout << "destroy instance" << std::endl;}
    
        // 删除拷贝构造函数和赋值运算符,确保单例唯一性
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    };
    
    Singleton* Singleton::m_pSingleton = getInstance();
    
  • 单例对象在程序启动后就进行了创建,多线程模式下,并不会出现线程安全问题,每个线程都是直接使用已经创建的单例对象。