0%

单例模式

简介

  • 单例模式相关学习笔记

单例模式

  • 单例模式,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

  • 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

  • 单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。

  • 注意:

    • 单例类只能有一个实例
    • 单例类必须自己创建自己的唯一实例
    • 单例类必须给所有其他对象提供这一实例
  • 单例模式是设计模式中最简单,最常见的一种。其主要目的是确保整个进程中,只有一个类的实例,并且提供一个统一的访问接口。常用于Logger类,通信接口类,线程池等。

基本原理

  • 限制用户直接访问类的构造函数,提供一个统一的public接口获取单例对象
  • 这里有一个先有鸡还是先有蛋的问题
    • 因为用户无法访问构造函数,所以无法创建对象
    • 因为无法创建对象,所以不能调用普通的getInstance()方法来获取单例对象
  • 解决这个问题的方法很简单,将 getInstance() 定义为static即可(这也会限制getInstance()内只能访问类的静态成员)

注意事项

  • 所有的构造函数是private
  • 拷贝构造,拷贝赋值运算符需要显示删除 =delete,防止编译器自动合成

C++单例模式的几种实现方式

版本一 饿汉式

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton1 {
public:
static Singleton1* getInstance() { return &inst; }
Singleton1(const Singleton1&) = delete;
Singleton1& operator=(const Singleton1&) = delete;

private:
Singleton1() = default;
static Singleton1 inst;
};

Singleton1 Singleton1::inst;
  • 这个版本在程序启动时创建单例对象,即使没有使用也会创建,浪费资源。

版本二 懒汉式

  • 通过将单例对象的实例化会推迟到首次调用getInstance(),解决版本一的问题
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Singleton2 {
    public:
    static Singleton2* getInstance() {
    if (!pSingleton) {
    pSingleton = new Singleton2();
    }
    return pSingleton;
    }
    Singleton2(const Singleton2&) = delete;
    Singleton2& operator=(const Singleton2&) = delete;

    private:
    Singleton2() = default;
    static Singleton2* pSingleton;
    };

    Singleton2* Singleton2::pSingleton = nullptr;

版本三 线程安全

  • 在版本二中,如果多个线程同时调用getInstance()则有可能创建多个实例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Singleton3 {
    public:
    static Singleton3* getInstance() {
    lock_guard<mutex> lck(mtx);
    if (!pSingleton) {
    pSingleton = new Singleton3();
    }
    return pSingleton;
    }
    Singleton3(const Singleton3&) = delete;
    Singleton3& operator=(const Singleton3&) = delete;

    private:
    Singleton3() = default;
    static Singleton3* pSingleton;
    static mutex mtx;
    };

    Singleton3* Singleton3::pSingleton = nullptr;
    mutex Singleton3::mtx;
  • 加锁可以解决线程安全的问题,但是版本三的问题在于效率太低,每次调用getInstance()都需要加锁,而加锁的开销又是相当高昂的

版本四 DCL(Double-Checked Locking)

  • 版本四是版本三的改进版本,只有在指针为空的时候才会进行加锁,然后再次判断指针是否为空。而一旦首次初始化完成之后,指针不为空,则不再进行加锁。既保证了线程安全,又不会导致后续每次调用都产生锁的开销
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Singleton4 {
    public:
    static Singleton4* getInstance() {
    if (!pSingleton) {
    lock_guard<mutex> lck(mtx);
    if (!pSingleton) {
    pSingleton = new Singleton4();
    }
    }
    return pSingleton;
    }
    Singleton4(const Singleton4&) = delete;
    Singleton4& operator=(const Singleton4&) = delete;

    private:
    Singleton4() = default;
    static Singleton4* pSingleton;
    static mutex mtx;
    };

    Singleton4* Singleton4::pSingleton = nullptr;
    mutex Singleton4::mtx;
  • DCL在很长一段时间内被认为是C++单例模式的最佳实践。但是也有文章表示DCL的正确性取决于内存模型。关于这部分的深入讨论可以参考以下两篇文章

版本五 Meyer’s Singleton

  • 这个版本利用局部静态变量来实现单例模式。最早由C++大佬,Effective C++系列的作者Scott Meyers提出,因此也被称为Meyers’ Singleton
  • TLDR: 这就是C++11之后的单例模式最佳实践,没有之一
    • 最简洁: 不需要额外定义类的静态成员
    • 线程安全:不需要额外加锁
    • 没有烦人的指针
1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton5 {
public:
static Singleton5& getInstance() {
static Singleton5 inst;
return inst;
}

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

private:
Singleton5() = default;
};
感谢老板支持!敬礼(^^ゞ