单例模式
特点:某些类,只具有一个对象(实例)称为单例,自行实例化并向整个系统提供这个实例,例如我们实现的线程池,缓存等。
常见的单例模式有懒汉模式和饿汉模式。
总结:
单例模式的特点:
(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操作:当需要更新数据时,判断当前内存值和之前所取得的值是否相等,如果相等用新值更新,若不相等则失败,失败就是重试,一般是一个自旋的过程,即不断重试。
当在临界资源待的时间比较短的时候,推荐使用自旋锁(自旋状态),当在临界资源待的时间比较长时,推荐使用挂起等待锁(之前我们所学习的基本都是挂起等待锁),因为如果在临界资源待的时间比较短,使用挂起等待锁要进行挂起和唤醒,花费的时间比较大,不推荐使用。