单例模式、读写锁

喜夏-厌秋 提交于 2020-03-02 10:18:06

单例模式

是设计模式中的一种,是大佬们针对典型场景设计的解决方案。

典型场景:一个对象/一个资源只能被初始化加载一次
例如:你在打游戏的时候,游戏有很多图片资源。希望一个图片只加载一次。

实现方式饿汉/懒汉

  • 饿汉:所有资源在程序初始化阶段一次性完成初始化(资源在初始化的时候一次性全部加载,后续只需要使用就可以)
template<class T>
class single
{
   static T _data;//所以实例化的对象共用同一份资源
   T* get_instance()
  {
    return &_data;
  }
}

资源初始化的时候会慢一点,但是运行起来以后,会很流畅。

  • 懒汉:资源在使用的时候进行初始化(用到的时候再去加载,当然也要保证只加载一次)
template <typename T>
class Singleton
{
   static T* inst;
public:
   static T* GetInstance() {
   if (inst == NULL) {
   inst = new T();
  }
  return inst;
 }
}
  • 在使用的时候加载一次资源,会涉及到线程安全问题。(volatile、static、mutex、二次判断)
  • 使用static保证多个对象使用同一份空间资源。(保证资源只被加载一次,实现单例模式)
  • 加锁保护资源申请过程,实现线程安全。(加锁保护,防止竞态条件下,资源被加载多次)
  • 在加锁之外进行二次判断,减少锁冲突概率,提高效率。
  • 使用vliatile关键字,防止编译器过度优化,每次判断都为NULL的情况。

锁冲突:

资源没有加载的时候,加锁进行判断,然后申请资源,解锁。

资源被加载成功之后,获取资源的时候平白多了两步加锁解锁操作,并且在加锁成功后,其它线程不能加锁,阻塞直到解锁(又平白多了一步等待加锁的时间)。二次判断就可以减少这种操作,不为空就直接获取资源进行操作。

#include< mutex >
class single
{
volatile static T * _data;
T *get_instance(){
if(_data==NULL){//外部进行二次判断,尽可能降低锁冲突概率
mutex,lock();
if(_data==NULL)
_data=new T();//实例化资源的过程是一个非原子操作,需要保护起来
mutex.unlock();
}
return _data;
}

STL中的容器是否是线程安全的?

不是。
原因是:
a.STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响。
b.对于不同的容器,加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶)。
c.因此 STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。

智能指针是否是线程安全的?

对于unique_ptr,由于只是在当前代码块范围内生效, 因此不存在线程安全问题。
对于
shared_ptr==, 多个对象需要共用一个引用计数变量,所以会存在线程安全问题。 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数。

其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试
  • 读写锁、自旋锁

读写锁

读写锁:多人读,少量写的场景。写互斥,读共享的场景。
写的时候,有人读,会出现读的不完整的情况。有人写,则会造成写冲突的场景。
所以写的时候其他线程不能读也不能写。
读的时候,大家可以一起读,只是读的时候别人不能写。

针对这种场景,使用一般的互斥锁已经不能满足了。使用读写锁实现这种场景的数据操作保护。

  • 加读锁:当前没有人写,就能加读锁。就是读者计数+1,加读锁的前提就是写者计数为0。
  • 加写锁:当前既没有人读,也没有人写,解锁。就是写者计数+1,加写锁的前提就是读者和写者计数都为0。

读写锁的实现原理:一个读者计数器+一个写者计数器
读写锁中若不能加锁,就需要等待。但是这里的等待和互斥锁的等待是不一样的。

互斥锁的等待:挂起等待(将pcb位置置位阻塞)
读写锁的等待:自旋等待(通过自旋锁实现)

自旋锁

自旋锁:一直占用cpu进行条件判断直到条件满足。
响应速度比较快,但是比较占用cpu资源(一直循环判断),适用于确定等待时间比较短的场景。
这里举一个例子来说明:
小明请女朋友吃饭,女朋友让小明等着,自己马上就下楼。小明就在楼下等着,等了半个小时之后,女朋友没有下楼,小明给女朋友打电话,女朋友说,再等一会,自己马上就下楼,然后小明又等了继续等待,直到女朋友下楼。
女朋友说马上就下楼,所以小明要一直等。别的什么事情都不能干,只能等待女朋友。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!