从C++单例模式到线程安全详解

发布时间 - 2026-01-10 21:57:24    点击率:

先看一个最简单的教科书式单例模式:

class CSingleton
{
public:
	static CSingleton* getInstance()
	{
		if (NULL == ps)
		{//tag1
			ps = new CSingleton;
		}
		return ps;
	}

private:
	CSingleton(){}
	CSingleton & operator=(const CSingleton &s);
	static CSingleton* ps;
};

CSingleton* CSingleton::ps = NULL;

有2个要点:

1.private的构造函数和=操作符,用于防止类外的实例化和被复制;

2.static的类指针和get方法。

在大多数单线程情况下,以上代码大都会运行得很好,除非遇到中断:

1.当程序运行到tag1 处触发了中断;
2.中断处理程序恰调用的也是getInstance函数。

可想而知,这和多线程的情况类似,假设线程A 运行到tag1处,还没来得及new,此时ps仍然是NULL,线程B(或中断处理程序) 同时也运行到此通过if判断,那么将会实例化2个CSingleton对象,显然是不对的。

为了解决上述问题,自然而然,最容易想到也最常用的方法是加锁,因此getInstance改成这样:

	static CSingleton* getInstance()
	{
		lock();//伪代码
		if (NULL == ps)
		{
			ps = new CSingleton;
		}
		return ps;
	}

加了锁以后貌似解决了上述问题,但也同样带来了新的问题:如果程序到处是诸如:

CSingleton::instance()->aaaa();
CSingleton::instance()->bbbb();
CSingleton::instance()->cccc();

这样的调用,除了第一次的lock()有用外,后面的都是在做无用功,lock()的代价说大不大,但在某些情况下还是会提高程序延迟,这对追求完美的程序猿来说是完全无法接受的。

于是乎,咱想出了一个办法:

	static CSingleton* getInstance()
	{
		if (NULL == ps)//这里加了次判断,只有第一次才会为true而调用lock()
		{
			lock();//伪代码
			if (NULL == ps)
			{
				ps = new CSingleton;
			}
		}
		return ps;
	}

很久以后我才知道,这个方法有个很高大上的名字,叫做双重检查锁定模式,简称DCLP(Double Checked Locking Pattern)。

DCLP很好地解决了多次调用不必要的lock()。

然而,你们以为这样就完了?too young。。

DCLP在多线程下仍然存在2个根本问题:

1.程序的指令执行顺序不确定;
2.编译器优化问题。

先说2,在某些编译器下,以上的两个if判断只会执行一个,甚至一个都不执行,原因是编译器认为至少有一个if判断是多余的,它自动帮助我们优化了代码。

再说1,ps = new CSingleton; 这条语句会被拆分为这样的三个步骤执行:

1.为要new的对象开辟一块内存;
2.构造该对象,填入这块内存;
3.将ps指针指向这块内存。

以上三个步骤,2和3的顺序是不确定的,可能先2后3,也可能先3后2。。。

实际执行时可能是这样的:

	static CSingleton* getInstance()
	{
		if (NULL == ps)
		{
			lock();//伪代码
			if (NULL == ps)
			{    //伪代码
				ps = xx;//step 3
				new sizeof(CSingleton);//step 1
				new CSingleton;//step 2
			}
		}
		return ps;
	}

如果编译器按上述顺序执行代码,考虑如下状况:

线程A 执行到step 1还未执行后面的step 2,此时ps非空,但其指向的内存里面的内容还未被构造出来,于此同时线程B 进入这个函数,判断ps非空直接返回ps,但是调用者此时访问的ps内存实际内容CSingleton还没被构造呢,这是一块地址正确大小正确但内部数据不明的东西,当然会出错(调用者一般这么调用:CSingleton::getInstance()->aa();  CSingleton::getInstance()->bb();  CSingleton::getInstance()->cc();........此时的aa,bb,cc是啥玩意儿?)。

这也是为什么加上volatile关键字仍然不可以解决同步问题,volatile只解决了编译器优化问题,却无法控制机器指令执行顺序。

很遗憾的是,C/C++本身在设计时是不考虑多线程问题的,也就是说,要处理多线程问题还要程序猿自己想办法填坑。。

说了这么多,我们要讨论的问题仍然没有解决,庆幸的是,C++ 11提供了内存栅栏技术来解决这个问题,这里不赘述,有兴趣的读者可以自己搜索资料看看,不过是一些api调罢了。

那么,C++ 11 以前的代码如何解决这个问题呢?很不幸,并没有很好的解决方案,一种可行的方案是,程序中不要到处这么调用这个单例对象:

CSingleton::getInstance()->aa(); 
CSingleton::getInstance()->bb();
CSingleton::getInstance()->cc();

而是在程序开始就初始化缓存这个单例对象:

CSingleton* const g_ps = CSingleton::getInstance();//程序一开始就缓存这个单例对象
g_ps->aa();
g_ps->bb();
g_ps->cc();

但是如此带来的问题是程序一开始就实例化了这个单例对象,对象在整个程序的声明周期存在,这貌似叫饿汉式,而之前那种叫懒汉式,孰轻孰重,只有根据实际情况取舍了。

以上就是小编为大家带来的从C++单例模式到线程安全详解全部内容了,希望大家多多支持~


# c  # 单例模式  # 线程安全  # 详解C++实现线程安全的单例模式  # C++线程安全的单例模式讲解  # 老生常谈C++的单例模式与线程安全单例模式(懒汉/饿汉)  # 详解如何使用C++写一个线程安全的单例模式  # 很好  # 多线程  # 的是  # 是在  # 还没  # 解决了  # 这块  # 不确定  # 解决这个问题  # 这是  # 情况下  # 都不  # 有个  # 将会  # 是这样  # 调用者  # 说了  # 这么多  # 不可以  # 但在 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: 深圳网站制作的公司有哪些,dido官方网站?  阿里云网站搭建费用解析:服务器价格与建站成本优化指南  西安市网站制作公司,哪个相亲网站比较好?西安比较好的相亲网站?  Swift中switch语句区间和元组模式匹配  html5怎么画眼睛_HT5用Canvas或SVG画眼球瞳孔加JS控制动态【绘制】  Laravel怎么实现验证码功能_Laravel集成验证码库防止机器人注册  Laravel项目怎么部署到Linux_Laravel Nginx配置详解  如何快速生成专业多端适配建站电话?  Laravel如何集成第三方登录_Laravel Socialite实现微信QQ微博登录  Laravel N+1查询问题如何解决_Eloquent预加载(Eager Loading)优化数据库查询  浏览器如何快速切换搜索引擎_在地址栏使用不同搜索引擎【搜索】  免费的流程图制作网站有哪些,2025年教师初级职称申报网上流程?  如何在Windows服务器上快速搭建网站?  网站制作大概要多少钱一个,做一个平台网站大概多少钱?  Laravel怎么解决跨域问题_Laravel配置CORS跨域访问  如何在IIS中新建站点并配置端口与IP地址?  百度输入法ai面板怎么关 百度输入法ai面板隐藏技巧  深圳网站制作设计招聘,关于服装设计的流行趋势,哪里的资料比较全面?  Laravel怎么设置路由分组Prefix_Laravel多级路由嵌套与命名空间隔离【步骤】  laravel怎么配置和使用PHP-FPM来优化性能_laravel PHP-FPM配置与性能优化方法  Python制作简易注册登录系统  Laravel如何使用Service Provider注册服务_Laravel服务提供者配置与加载  C++时间戳转换成日期时间的步骤和示例代码  详解Oracle修改字段类型方法总结  Edge浏览器怎么启用睡眠标签页_节省电脑内存占用优化技巧  如何自定义建站之星模板颜色并下载新样式?  iOS正则表达式验证手机号、邮箱、身份证号等  laravel怎么使用数据库工厂(Factory)生成带有关联模型的数据_laravel Factory生成关联数据方法  Laravel如何从数据库删除数据_Laravel destroy和delete方法区别  如何快速打造个性化非模板自助建站?  详解Android——蓝牙技术 带你实现终端间数据传输  iOS中将个别页面强制横屏其他页面竖屏  Laravel的HTTP客户端怎么用_Laravel HTTP Client发起API请求教程  HTML5空格和margin有啥区别_空格与外边距的使用场景【说明】  如何用5美元大硬盘VPS安全高效搭建个人网站?  laravel怎么在请求结束后执行任务(Terminable Middleware)_laravel Terminable Middleware请求结束任务执行方法  mc皮肤壁纸制作器,苹果平板怎么设置自己想要的壁纸我的世界?  成都网站制作公司哪家好,四川省职工服务网是做什么用?  如何用VPS主机快速搭建个人网站?  大学网站设计制作软件有哪些,如何将网站制作成自己app?  手机软键盘弹出时影响布局的解决方法  Laravel的.env文件有什么用_Laravel环境变量配置与管理详解  Laravel如何实现URL美化Slug功能_Laravel使用eloquent-sluggable生成别名【方法】  jQuery中的100个技巧汇总  如何快速搭建高效WAP手机网站吸引移动用户?  Laravel Fortify是什么,和Jetstream有什么关系  uc浏览器二维码扫描入口_uc浏览器扫码功能使用地址  javascript基本数据类型及类型检测常用方法小结  JS弹性运动实现方法分析  手机网站制作与建设方案,手机网站如何建设?