线程安全的单例模式
- 一.STL和智能指针的安全
- 二.单例模式
- 1.基本概念
- 2.懒汉和饿汉的实现方式
- 三.常见的其它锁
- 四.读者写者模型
一.STL和智能指针的安全
1.STL中的容器是否是线程安全的?
不是. 原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器,加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).因此 STL 默认不是线程安全. 如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。
2.智能指针是否是线程安全的?
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
二.单例模式
1.基本概念
1.什么是单例模式
单例模式是一种 “经典的, 常用的, 常考的” 设计模式。
2.单例模式的特点
某些类, 只应该具有一个对象(实例), 就称之为单例。
3.单例模式常见的两种设计方式
饿汉方式和懒汉方式。
举个例子:
吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.
换言之,当我们申请空间时,饿汉是立马申请;懒汉是延时申请(当我们真的需要使用时再申请)。懒汉的优势在于可以提高我们的运行速度,因为它把申请空间的所需的时间延后了,所以我们当前运行就会变快。再例如打开一个游戏,10个G,懒汉先加载要使用的,剩下的等需要时再加载,这样游戏的运行速度变快了。
2.懒汉和饿汉的实现方式
伪代码
两者的主要区别在于,一个是直接创建对象,一个是先创建的对象指针,等需要时再new一个对象。static修饰的是静态变量,也是全局变量的一种,也就意味着饿汉是在程序加载到内存时就要创建对象,而懒汉不需要,所以懒汉提高了运行效率。
懒汉模式的线程安全问题
很明显,在进行多并发时,懒汉模式会存在安全问题。当一个线程判断完if后,正准备new对象,结果就被替换了,接着又来一个线程进行if判断也能通过,这样就造成了new了多个对象。解决方法也很简单,在if前面加锁就行了。进一步优化一下:当第一个对象被new出来后,其实就不再需要锁了,这样重复的加锁,解锁也会对性能造成影响,所以再在锁的前面套一个if判断,下面是优化版本。
三.常见的其它锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
自旋锁:
这个锁与之前的锁有些区别,之前的锁都称作挂起等待锁,顾名思义就是获取锁失败后把自己挂起。但该锁不一样,它获取锁失败后不让自己挂起,而由线程周而复始的去申请锁,在申请锁的过程中不断检测锁的状态的过程就叫做自旋。
很明显,这个过程是消耗时间的,所以使用自旋锁一般在临界资源释放时间很短的情况下使用。
第一个函数申请锁失败后会直接挂起,而第二个函数申请锁失败后会返回失败。所以实现一个自旋锁直接用第二个函数再外加一个while循环即可。
当然,pthread库直接提供了自旋锁的接口。
自旋锁阻塞版本也并不是挂起,实际上就是底层封装了while循环,当申请锁失败后一直循环,看到的效果就是阻塞了。非阻塞版本就是没有封装while。
当然该锁也有初始化,解锁,销毁…与互斥锁基本一致。
总结:对于用户而言,自旋锁#其它锁区别在于一个不会将线程挂起,一个会挂起罢了。
四.读者写者模型
读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
加锁:读者加锁和写者加锁是不同的
写者。
读者。
读写者模型有一个特点:读者优先。因为读者的数量一般情况下是远远大于写者的,所以在竞争锁时,读者有更大的概率得到锁,可能会导致写者一直得不到锁,从而造成线程饥饿问题。注意:这里的线程饥饿是一个中性词,因为这本是它的特点。当然我们也可以通过修改代码来实现写者优先。
为了更好的理解,下面是伪代码