现代C++设计模式之单例模式

目录

1.动机

2.应用场景

3.实现方式

4.分类

5.UML类图

6.C++语言实现

(1)饿汉式单例

(2)懒汉式单例

朴素的懒汉式单例(非线程安全)

支持多线程的懒汉式单例(加互斥锁)

双重检验锁定模式(double-checked locking pattern)

利用 std::call_once() 函数实施线程安全的延迟初始化

针对加互斥锁模式问题的优化

针对双重检验锁问题的优化

1.动机

确保系统中只有唯一一个实例。

2.应用场景

配置文件管理器,日志记录器,数据库连接池,线程池,全局缓存,窗口管理器,硬件访问,任务管理器,负载均衡器

3.实现方式

    (1)将构造函数可见性改为private
    (2)定义静态变量私有成员变量,负责保存这个唯一实例
    (3)增加一个公有的静态方法,使外界可以使用并实例化该成员变量

4.分类

    (1)饿汉式:程序启动时实例化,线程安全,适合单例对象占用资源少,启动时就需用到该对象时使用。
    (2)懒汉式:延迟加载,使用时初始化,需要考虑线程同步,适合对象占用资源多,希望延迟加载时使用。

5.UML类图

6.C++语言实现

(1)饿汉式单例
#include<iostream>
using namespace std;
/*单例模式:构造函数私有化,对外提供一个接口*/
//饿汉模式:不管用不用得到,都构造出来。本身就是线程安全的
class ehsingleClass {
public:
    static ehsingleClass* getinstance()
    {
        return instance;
    }
private:
    static ehsingleClass* instance;//静态成员变量必须类外初始化,只有一个
    ehsingleClass() {}
    ehsingleClass(ehsingleClass const &) = delete;
    ehsingleClass& operator=(ehsingleClass const &) = delete;
};
ehsingleClass* ehsingleClass::instance = new ehsingleClass();
//类外定义,main开始执行前,该对象就存在了
int main()
{
    //饿汉模式
    ehsingleClass* ehsinglep3 = ehsingleClass::getinstance();
    ehsingleClass* ehsinglep4 = ehsingleClass::getinstance();
    //ehsingleClass* ehsinglep5 = ehsingleClass::get();//非静态成员方法必须通过对象调用,不能通过类域访问
    cout << ehsinglep3 << endl;
    cout << ehsinglep4 << endl;
    system("pause");
    return 0;
}
(2)懒汉式单例
  • 朴素的懒汉式单例(非线程安全)
#include <iostream>

class LazySingleton {
private:
    static LazySingleton* instance;
    LazySingleton() {}
    LazySingleton(LazySingleton const &) = delete;
    LazySingleton& operator=(LazySingleton const &) = delete;

public:
    static LazySingleton* get_instance() {
        if (instance == nullptr) {
            instance = new LazySingleton();
        }
        return instance;
    }
};

LazySingleton* LazySingleton::instance;

int main(void) {

    LazySingleton* a = LazySingleton::get_instance();
    LazySingleton* b = LazySingleton::get_instance();

    std::cout << a << std::endl;
    std::cout << b << std::endl;

    return 0;
}
输出:
0x8b8eb0
0x8b8eb0
  • 支持多线程的懒汉式单例(加互斥锁)
#include <iostream>
#include <mutex>

class LazySingleton {
private:
    static LazySingleton* instance;
    static std::mutex m;
    LazySingleton() {}
    LazySingleton(LazySingleton const &) = delete;
    LazySingleton& operator=(LazySingleton const &) = delete;

public:
    static LazySingleton* get_instance() {
        std::lock_guard lk(m);
        if (instance == nullptr) {
            instance = new LazySingleton();
        }
        return instance;
    }
};

LazySingleton* LazySingleton::instance;
std::mutex LazySingleton::m;

int main(void) {

    LazySingleton* a = LazySingleton::get_instance();
    LazySingleton* b = LazySingleton::get_instance();

    std::cout << a << std::endl;
    std::cout << b << std::endl;

    return 0;
}
        这段代码使用了 std::mutex 互斥,使得单例类线程安全了,现在可以保证是真正的“单例”了。
但问题也很明显,一旦有一个线程获取了锁,直到其释放锁前,其他线程都会阻塞。也就是说,即便单例对象早已经创建完成,其他的线程进行到这的时候还是经常会被阻塞,并发性能极差。如果并发线程多,可能导致大量线程在这里阻塞,甚至拖慢整个系统的效率。
  • 双重检验锁定模式(double-checked locking pattern)
        双重检验锁定模式简单地说,与上面的方法的区别是:上面的方法中,是先获取锁,再检查空指针,如果指针为空,则创建对象实例;而双重检验锁定模式重,先检查一次空指针,如果指针为空,则获取锁,再检查指针,如果仍为空,则创建对象实例。
        我们修改 get_instance() 方法如下:
static LazySingleton* get_instance() {
    if (instance == nullptr) {
        std::lock_guard lk(m);
        if (instance == nullptr) {
            instance = new LazySingleton();
        }
    }
    return instance;
}
        这个改动的好处是,只有当指针为空的时候,才会获取锁。而在之前的方法中,不管指针是否为空都要先获取锁。而在获取锁后,还会再判断一次(即双重检验)指针是否为空,这是防止在第一个判断和获取锁之间,有其他进程改动过指针。
        此方法减少了获取锁的可能,就减少了线程并发的代价,同样实现了线程安全,且有一定的性能提升。不过我认为这个优化带来的性能提升还是比较有限的
        但是,请注意,双重检验锁定模式并非适合所有场景。
        双重检验锁定模式多用于延迟初始化,我们上面的懒汉式单例也属于这种,只是过于简单,其创建实例的过程非常快,难以体现出此方法的弊端。
        假设我们需求如下:通过 get_instance() 方法获取单例实例,如果实例不存在(指针为空),则创建实例,对实例对象进行一些初始配置(修改),然后返回指向实例的指针。
需求看似很简单,但如果我们再加一条要求呢:初始化配置过程应该只进行一次,且耗时非常长! 这并不是什么苛刻离谱的要求,而是一个合理合情的需求。
        想象一下,如果我们将耗时很长的初始化过程也放在获取锁内的时间内执行,固然可以完成上述需求,但此时除了正在执行初始化配置以外的其他线程,调用 get_instance() 时都会阻塞,最终结果就是大量线程阻塞,导致系统整体性能下降。所以我们需要将耗时的初始化过程放在锁外进行。
        但锁外执行初始化操作,也存在问题,我们看下面的代码:
static LazySingleton* get_instance() {
    if (instance == nullptr) {
        std::lock_guard lk(m);
        if (instance == nullptr) {
            instance = new LazySingleton();
        }
    }
    // initialize something
    // takes a long time
    return instance;
}
假设我们现在不存在实例,有两个线程 A 和 B 开始调用 get_instance()
  1. 线程 A 执行到第 2 行的  if (instance == nullptr),然后线程 B 开始执行;
  2. 线程 B 一口气执行完了  get_instance() 全部的内容,即最终创建了一个单例对象,并做了一些初始配置(由第 7、8 行注释表示),并给调用者返回了指向实例的指针。
  3. 线程 A 继续执行,执行到第二重检查时(第 4 行),发现指针已非空,于是跳到了第 7 行,开始初始化配置。
        问题就在线程 A 最后这个初始化配置,线程 B 将实例指针返回给调用者后,该实例是有可能被修改的。而线程 A 在我们计划外的,对实例进行了第 2 次初始化,使得结果偏离预期。
上述场景,双重检验锁定模式就不合适了,我们看下一个。
  •  利用 std::call_once() 函数实施线程安全的延迟初始化
        在讲这一节前,我们需要先介绍一下,std::once_flag 类 和 std::call_once() 函数,他们均在 C++11 中引入,在 “mutex” 头文件中。 std::once_flag 类是函数 std::call_once() 的辅助类,传递给多个 std::call_once() 调用的 std::once_flag 对象让这些调用相互协调,使得最终仅有一个调用真正运行完成。这个类不可复制亦不可移动。
        std::call_once() 方法接受一个 std::once_flag 引用,以及一个可调用类型 f 及其参数。如果有多个 std::call_once() 调用接受了同一个 std::once_flag 引用,那么所有的 std::call_once() 调用只会有一个执行完成。另外要注意,如果多个传入的函数调用不一样,也是只会调用一次,但实际调用哪个是不确定的,属于未定义行为。
        函数 std::call_once() 的声明如下:
template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
        Callable 指的是任何可调用类型(callable type,包含函数指针、函数对象、lambda 等,能让适用者对其进行函数调用操作)
        总之,到这里,我们知道了一件事:C++11 标准库中提供了方法,可以让某件事只做一次(比如初始化一次),不管调用了几次,不管是不是多线程调用的,最终一定只执行一次。并且可以保证这一次调用执行完成后,所有线程才会继续推进。
  • 针对加互斥锁模式问题的优化
        在 加互斥锁的懒汉式单例中,我们说过那个方法会可能导致有很多线程阻塞。这个问题的主要原因是,我们先获取锁,再检查指针,导致不必要的获取锁和阻塞。
        我们这里通过一个 create_instance_flag,让多个线程都调用 std::call_once(),保证对象的创建由其中某线程安全唯一的完成(通过合适的同步机制,有可能是系统提供的方法,也可能是加锁等)。这样做的性能比其他同类操作要好
#include <iostream>
#include <mutex>
#include <thread>

class LazySingleton {
private:
    static LazySingleton* instance;
    static std::once_flag create_instance_flag;
    LazySingleton() {}
    LazySingleton(LazySingleton const &) = delete;
    LazySingleton& operator=(LazySingleton const &) = delete;
    static void create_instance() {
        instance = new LazySingleton();
    }

public:
    static LazySingleton* get_instance() {
        std::call_once(create_instance_flag, create_instance);
        return instance;
    }
};

LazySingleton* LazySingleton::instance;
std::once_flag LazySingleton::create_instance_flag;

int main(void) {
    return 0;
}
  • 针对双重检验锁问题的优化
        对于双重检验锁模式提到的,将耗时的初始化内容与对象的创建操作分离,但同时保证只执行一次,我们可以再加入一个  init_instance_flag 用于保证这一点
#include <iostream>
#include <mutex>
#include <thread>

class LazySingleton {
private:
    static LazySingleton* instance;
    static std::once_flag create_instance_flag;
    static std::once_flag init_instance_flag;
    LazySingleton() {}
    LazySingleton(LazySingleton const &) = delete;
    LazySingleton& operator=(LazySingleton const &) = delete;
    static void create_instance() {
        instance = new LazySingleton();
    }
    static void init_instance() {
        // initialize something
        // takes a long time
    }

public:
    static LazySingleton* get_instance() {
        std::call_once(create_instance_flag, create_instance);
        std::call_once(init_instance_flag, init_instance);
        return instance;
    }
};

LazySingleton* LazySingleton::instance;
std::once_flag LazySingleton::create_instance_flag;
std::once_flag LazySingleton::init_instance_flag;

int main(void) {
    return 0;
}
  • 13
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值