Linux:线程安全的单例模式及STL、智能指针与线程安全

单例模式

特点:某些类,只具有一个对象(实例)称为单例,自行实例化并向整个系统提供这个实例,例如我们实现的线程池,缓存等。
常见的单例模式有懒汉模式和饿汉模式。
总结
单例模式的特点:
(1)单例类只能有一个实例
(2)单例类必须创建自己的唯一实例
(3)单例类必须给其他对象提供这一对象实例
单例模式的优点:
(1)单例模式只能创建一个对象,所以在资源方面可以做到节约资源
(2)单例模式不需要频繁地销毁和创建,所以在效率方面有所提高
(3)单例对象在整个系统里面只有一份,可以做到避免共享资源的重复占用
(4)单例模式的对象必须向整个系统提供,所以可以做到全局

  • 饿汉模式
    在程序初始化时加载一次资源,运行过程就不再重新加载了
    例如:
class Singleton
{
public:
	static Singleton& GetInstrance()
	{
		return m_ins;
	}
private:
	Singleton()
	{}
	static Singleton m_ins;//程序启动时对象创建好,通过Singleton这个包装类来使用T对象,一个进程中只有一个T对象的实例
	Singleton(const Singleton&) = delete;
	Singleton& operator=(Singleton const&) = delete;
};
Singleton Singleton::m_ins;//初始化
int main()
{
	Singleton& s = Singleton::GetInstrance();
	return 0;
}

分析:如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好,且加载进行时静态创建单例对象,线程安全。缺点是无论是否使用,总要创建,浪费内存

  • 懒汉模式
    懒汉模式最核心的思想就是“延时加载”,从而优化服务器的启动速度(例如写时拷贝)
    如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
    优点是:什么时候用什么时候创建,节约内存,,缺点是在第一次调用访问获取实例对象的静态接口时才真正创建,如果在多线程操作情况下有可能被创建出多个实例化对象,存在线程不安全问题
    它的实现方式有两种:(1)静态指针+用到时初始化;(2)局部静态变量
    为什么叫他懒汉模式,就是不到调用GetInstrance函数,这个类的对象就是不存在的
    我们下面写的是静态指针写法
    例如:
#include <mutex>
#include <thread>
//懒汉模式,第一次使用时创建,延迟加载
//不是线程安全的,不能保证只能创建一个对象,因此实现加锁功能
//容易造成线程阻塞,利用双判断
//volatile作用是禁止编译器对代码发生指令重排
class Singleton
{
public:
	static volatile Singleton* GetInstrance()
	{
		if (nullptr == m_ins)//加一层检测,防止线程阻塞
		{
			m_mutex.lock();//加锁
			if (nullptr == m_ins)//若为空,说明是第一次调用
				m_ins = new Singleton;
			m_mutex.unlock();//解锁
		}
		return m_ins;
	}
	class GC
	{
	public:
		~GC()
		{
			if (m_ins)
			{
				delete m_ins;
				m_ins = nullptr;
			}
		}
	};//内嵌垃圾回收类
private:
	Singleton()
	{}

	Singleton(const Singleton&) = delete;
	static volatile Singleton* m_ins;//在使用时创建对象
	static mutex m_mutex;
	static GC m_gc;
};
volatile Singleton*  Singleton::m_ins = nullptr;
mutex Singleton::m_mutex;
Singleton::GC m_gc;

分析:懒汉模式是在第一次使用时创建对象,上述volatile的作用是防止过度优化及防止指令重排,线程每次获取volatile变量的值都是最新的;并且要记得加锁,因为如果不加锁,有可能在调用GetInstance时,如果两个线程同时调用,可能会创建出两份T对象的实例,因此要注意线程安全。使用双重if判定,避免不必要的锁竞争。

STL,智能指针和线程安全
  • STL中的容器
    STL中的容器不是线程安全的,因为STL的设计初衷是将性能挖掘到极致,一旦涉及加锁保证线程安全,会对性能产生巨大的影响,而且对于不同的容器,加锁方式的不同,性能也可能不同,因此STL默认不是线程安全的,如果实在多线程环境下使用,往往需要调用者自行保证线程安全。
  • 智能指针
    对于unique_ptr,因为只在当前代码块内生效,因此不涉及线程安全问题,对于shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题,但是标准库实现的时候已经考虑到了此问题,基于原子操作保证shared_ptr能够高效,原子的操作引用计数。
  • 其他常见的锁
    悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁、写锁、执行锁等),当其他线程想要访问数据时,被阻塞挂起(例如互斥锁)。
    乐观锁:每次取数据时,总是乐观的以为数据不会被其他数据修改,因此不上锁,但是在更新数据前,会判断其他数据在更新前是否对数据进行修改,主要采用2种方式:版本号机制和CAS操作。
    CAS操作:当需要更新数据时,判断当前内存值和之前所取得的值是否相等,如果相等用新值更新,若不相等则失败,失败就是重试,一般是一个自旋的过程,即不断重试。
    当在临界资源待的时间比较短的时候,推荐使用自旋锁(自旋状态),当在临界资源待的时间比较长时,推荐使用挂起等待锁(之前我们所学习的基本都是挂起等待锁),因为如果在临界资源待的时间比较短,使用挂起等待锁要进行挂起和唤醒,花费的时间比较大,不推荐使用。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值