一文搞懂单例模式

引言:
对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。
要点:
显然单例模式的要点有三个;
一是某个类只能有一个实例;
二是它必须自行创建这个实例;
三是它必须自行向整个系统提供这个实例。

从具体实现角度来说,就是以下三点:
一是单例模式的类只提供私有的构造函数,
二是类定义中含有一个该类的静态私有对象,
三是该类提供了一个静态的公有的函数用于创建或获取它本身的静态私有对象。

单例模式分为 饿汉模式(线程安全) 和 懒汉模式:
懒汉式 单线程 使用时才回去构造 最基础的实现方式,线程上下文单例,不需要共享给所有线程,也不需要加synchronize之类的锁,以提高性能。
饿汉式 多线程 不管用不用都会创建 加上synchronize之类保证线程安全的基础上的懒汉模式,相对性能很低,大部分时间并不需要同步

懒汉模式
class Singleton
{
private:
    Singleton(){}

    static Singleton* instance;
public:
    static Singleton* getInstance()
    {
        if(instance == NULL){
            instance = new Singleton();
        }
        return instance;
    }
}
Singleton* Singleton::instance = NULL;

/*
在多线程场景中这样的代码存在一个问题:

  1. 当某个线程调用getInstance并执行到new Singleton时恰好被挂起,
  2. 此时另一个线程也调用getInstance发现instance仍然为空,又会调用new Singleton
  3. 于是就产生了多个Singleton实例
    */
饿汉模式

支持多线程

class Singleton
{
private:
    Singleton(){}

    static Singleton* instance;
    static pthread_mutex_t mutex;
public:
    static Singleton* getInstance();
    
}
Singleton* Singleton::instance = NULL;
pthread_mutex_init(&Singleton::mutex);

Singleton* Singleton::getInstance()
{
        pthread_mutex_lock(&mutex);
        if(instance == NULL){
            instance = new Singleton();
        }
        pthread_mutex_unlock(&mutex);
        return instance;
}

但这样带来一个问题,我们只想在是否创建新的类的问题上用锁,但引发的后果是第二次以及之后的访问都会加锁解锁,锁的开销是很大的,当访问次数很多时,明显会带来巨大的开销

double-check
在懒汉式基础上仅第一次创建时同步,性能相对较高,有效避免了锁调用时的阻塞等待
class Singleton
{
private:
    Singleton(){}

    static Singleton* instance;
    static pthread_mutex_t mutex;
public:
    static Singleton* getInstance();
    
}
Singleton* Singleton::instance = NULL;
pthread_mutex_init(&Singleton::mutex);

Singleton* Singleton::getInstance()
{
    if(instance == NULL){
        pthread_mutex_lock(&mutex);
        if(instance == NULL){
            instance = new Singleton();
        }
        pthread_mutex_unlock(&mutex);
        return instance;
    }
}

等效于:

volatile T* pInst = 0;
T* GetInstance()
{
    if(pInst == NULL){
        lock();
        if(pInst == NULL){
            pInst = new T;
        }
        unlock();
    }
}
  1. 当函数返回时,pInst 总是指向一个有效的对象。而lock和unlock防止了多线程竞争导致的麻烦。
  2. 双重的if的妙用:让lock的调用开销降到最小。因为pInst只是初始化的时候需要加锁。
问题
那么问题来了: CPU会乱序执行。
C++中的new其实其实包含了两个步骤:

分配内存
调用构造函数
所以 pInst = new T; 包含了三个步骤:

分配内存
在内存的位置上调用构造函数
将内存的地址赋值给pInst
其中23的顺序可以颠倒,出现这样的情况: pInst的值已经不是NULL,但对象仍然没有构造完毕。这时候如果出现另外一个对GetInstance的并发调用,此时第一个if内的表达式pInst==NULLfalse,所以这个调用会直接返回尚未构造完全的对象的地址(pInst)以给用户使用。这时候就可能会引发崩溃。
改进
#define barrier() __aum__ volatile ("lwsync")
  1. __asm__表示这个是一条汇编命令
  2. valotial关键字表示要求编译器严格按照这个顺序进行编译,不允许改变执行顺序
  3. low降低 synchronization同步,同时性
volatile T* pInst = 0;
T* GetInstance()
{
    if(pInst == NULL){
        lock();
        if(pInst == NULL){
            T* temp = new T;
            barrier();
            pInst = temp;
        }
        unlock();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值