设计模式——单例模式(懒加载)

本文详细探讨了单例模式的懒加载实现,分析了不同版本的线程安全问题和效率问题。从最初的线程不安全版本,到使用互斥锁解决线程安全,再到双if优化单线程效率,最后引入volatile和barrier防止编译器及CPU优化带来的影响。文章旨在揭示如何在保证线程安全的同时,优化单例模式的性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这篇文章总结几种比较常用的设计模式,,,不懂得设计模式。。。怎么敢称熟悉OOP思想。

单例模式的核心结构中只包含一个被称为单例类的特殊类,通过单例模式可以保证系统中一个类只有一个实例

由于快加载单例模式是线程安全的,所以本文只讨论懒加载单例模式的线程安全问题

版本一 使用懒加载(快加载),程序调用时再分配内存,然后初始化


class Singleton
{
public:
    static Singleton* getInstance()  //该公有接口专门返回对象实例
    {
        if(sobj == NULL)
        {
            sobj = new Signleton();//懒加载,懒加载,程序调用时才生分配内存
        }
        return sobj;
    }
private:
    Singlenton(){};  //私有化构造函数
    static Singlenton *sobj;
}
signlenton* signlenton::sobj = NULL;//慢加载,程序调用时才会生成,然后初始化

上述代码存在线程安全问题,if语句非原子操作,多线程调用时,会存在竞态条件

版本二 使用懒加载,考虑到多线程安全问题,使用互斥锁可以解决

1,考虑多线程调用时,是否存在竞态条件
2,考虑竞态条件,是否随着线程调度顺序的不同,而出现了不同的运行结果
3,发生竞态条件的代码段称为临界区代码段,临界区代码段都满足原子操作,为了保证原子操作,需要给临界区的代码段加上互斥锁进行控制,if语句不是原子操作,需要加互斥锁
4,竞态条件:计算的正确性取决于多个线程的交替执行时许时,就会发生竞态条件。例如:1,由于多个线程的执行时序是不确定的,从而导致执行结果出现各种问题。2,延迟初始化, (单例模式)懒加载模式,只有在使用时才初始化


class Singleton
{
public:
    static Singleton* getInstance()  //该公有接口专门返回对象实例
    {
        pthread_mutex_lock(&mutex);//加锁
        if(sobj == NULL)
        {
            sobj = new Signleton();//懒加载,程序调用时才生分配内存
        }
        pthread_mutex_unlock(&mutex);//解锁
        return sobj;
    }
    ~Singlenton()
    {
        pthread_mutex_destroy(&mutex);//锁的销毁
    }
private:
    Singlenton(){};  //私有化构造函数
    static Singlenton *sobj;
    static ptheread_mutex_t mutex;
}
signlenton* signlenton::sobj = NULL;//懒加载,程序调用时才会生成,然后初始化
pthread_mutex_t Singleton::mutex = PTHREAD_MUTEX_INITIALIZER;//锁的初始化

上述互斥锁满足了 if语句的原子操作,实现了线程安全,但由于程序每调用一次,就会产生加锁解锁操作,对于单线程而言,降低了效率,所以代码还需要解决单线程的效率问题

版本三 由于存在单线程和多线程共存问题,提高单线程效率,还要满足多线程的线程安全问题,需要使用双if语句


1,单线程只需要执行一次互斥锁的加锁解锁,之后的第一个if语句都是返回false
2,多线程仍需要注意竞态条件和线程安全问题

class Singleton
{
public:
    static Singleton* getInstance()  //该公有接口专门返回对象实例
    {
        if(sobj == NULL)  //双if语句,单线程第二次调用不用进行加锁解锁操作
        {
            pthread_mutex_lock(&mutex);//加锁
            if(sobj == NULL)
            {
                sobj = new Signleton();//懒加载,程序调用时才生分配内存
            }
            pthread_mutex_unlock(&mutex);//解锁
        }
        return sobj;
    }
    ~Singlenton()
    {
        pthread_mutex_destroy(&mutex);//锁的销毁
    }
private:
    Singlenton(){};  //私有化构造函数
    static Singlenton *sobj;
    static ptheread_mutex_t mutex;
}
signlenton* signlenton::sobj = NULL;//懒加载,程序调用时才会生成,然后初始化
pthread_mutex_t Singleton::mutex = PTHREAD_MUTEX_INITIALIZER;//锁的初始化

上述代码提高了单线程效率,并实现线程是安全的,调用多线程时,当锁被释放后,线程缓存sobj的值刷新到原始内存中了,所以下次取值是在内存中取,是线程安全的。解决了线程安全问题,要使单例模式更加完美,还得考虑编译器和CPU所出现的优化问题。


版本四 考虑编译器的指令优化和CPU的动态指令优化

1,volatile关键字阻止编译器为了提高速度将一个变量存在寄存器内而不写回内存
2,volatile关键字阻止调整操作sobj变量的指令操作
3,volatile关键字可以告诉编译器此处的变量很有可能被其他地方改变,不能进行优化
4,barrier指令会阻止CPU对指令进行动态换序优化

class Singleton
{
public:
volatile static Singleton *getIntance()
{
if(sobj == NULL)
{
pthread_mutex_lock(&mutex);//多线程线程安全问题
if(sobj == NULL)//单线程效率问题
{
Singleton *tmp = new Singleton();
barrier();//防止CPU对指令进行动态换序优化,使对象的构造一定在barrier完成
//因此赋值给sobj的对象是完好的
sobj = temp;
}
pthread_mutex_unlock(&mutex);
}
return sobj;
}
private:
Singleton(){};
volatile static Singleton sobj;
};
volatile Singleton* Singleton::sobj = NULL; //懒加载,快加载

编译器很可能会为了效率而交换两条不相干的指令,使用volatile可以阻止。

我们为了单线程的效率而做个两个if判断,但是编译器很有可能会认为第一次判断成功以后就没有必要再判断第二次,因为编译器会认为sobj的值在这两次判断之间并没有改变,(编译器不会考虑另一个线程有没有改变project的值,他只看到这一段代码)但是这两次判断是有其作用的,我们并不希望编译器做这样的优化。

CPU动态指令换序问题,temp = new int; 实质上由三个步骤:分配内存,在内存的位置上调用构造函数,将内存的地址赋给指针sobj。由于CPU乱序执行,很有可能将步骤2和3颠倒,此时会出现一种情况就是;还未调用构造函数就把内存地址赋给指针,假如有另一个线程调用getinstance函数
那么由于sobj指针非空,该线程就会获取到这个非空但并没有构造完全的对象指针,如果使用该指针的话,会带来很恶劣的影响。barrier指令会阻止CPU对指令进行动态换序优化

以上就是小码哥所理解的 –》 单例模式–懒加载
欢迎大家的批评和建议,以后的每天,小码哥都会带大家在优快云的领域遨游,去享受码代码所带来的快乐

不啰嗦,最后一个环节关于两个关键字的最后一点:
/*
1,volatile 只是用于阻止编译器的一部分优化机制,与锁安全无关,线程安全不能依靠volatile来实现。最多只能作为线程安全的额外考虑。

2,barrier 要保证线程安全,即使我们使用了锁等同步操作,我们仍需考虑CPU的乱序执行,要保证线程安全,在适当的地方加上barrier,阻止CPU换序操作是必要的。
*/

码完,收工

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值