Linux 线程安全常用的锁机制
转自:https://blog.csdn.net/qq_35423154/article/details/109259881
文章目录
- 乐观锁 VS 悲观锁
- 悲观锁
- 乐观锁
- CAS
- CAS机制
- ABA问题
- CAS的优缺点
- 互斥锁 VS 自旋锁
- 互斥锁
- 自旋锁
- 对比及应用场景
- 读写锁
- 实现方式
- 读写锁 VS 互斥锁
乐观锁 VS 悲观锁
乐观锁和悲观锁故名思意,它们的区别就是做事的心态不同。
悲观锁
悲观锁做事比较悲观,它始终认为共享资源在我们使用的时候会被其他线程修改,容易导致线程安全的问题,因此在访问共享数据之前就要先加锁,阻塞其他线程的访问。
常见的例子就是数据库中的行锁、表锁、读锁、写锁等
乐观锁
乐观锁则于悲观锁相反,它则比较乐观。它始终认为多线程同时修改共享资源的概率较低,所以先不管三七二十一,改了再说。
乐观锁会直接对共享资源进行修改,但是在更新修改结果之前它会验证这段时间有没有其他线程对资源进行修改,如果没有则提交更新,如果有的话则放弃本次操作。
由于乐观锁全程没有进行加锁,所以它也被称为无锁编程,通常以CAS操作+版本号机制实现。
CAS
CAS机制
CAS是英文单词Compare And Swap的缩写,也就是比较和替换,这也正是它的核心。
CAS机制中用到了三个基本操作数,内存地址V,旧预期值A,新预期值B
当我们需要对一个变量进行修改时,会对内存地址V和旧预期值进行比较,如果两者相同,则将旧预期值A替换成新预期值B。而如果不同,则将V中的值作为旧预期值,继续重复以上操作,即自旋。
下面分别举出成功和失败的例子
此时内存地址中存储的值为9,线程1的旧预期值为9,新预期值为10,即我们要对里面的值进行一个加一操作
此时旧预期值与V相同,将B与V交换
此时修改成功。
接着看看修改失败的情况
此时V中的值为9,线程1中的旧预期值为9,想将V中的值修改为10
当我们正要开始修改时,突然一个线程抢先更新数据,此时V的值变为了14
由于此时A的值与V不同,我们就需要重新获取V中的值,并计算出新的预期值
此时两者相同,完成替换,V=15
从上面可以看出,CAS是乐观锁,它乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。
ABA问题
所谓的ABA问题,就是将一个变量从A变为了B,再从B变为了A。
假设我正在银行中提款,此时我的账户中有1000元,我想从中取出500元,但是由于忽然的网络波动,此时这个操作被重复了两次,于是如下图
此时我们只能执行第一个扣费,由于执行完后A != V,所以第二个线程即不断自旋比较
此时正好舍友还了你几年前借的500块钱,你的金额又重新变为了1000
这时线程2又给你扣了500,于是你取出了500块钱,却意外的扣了1000
那么这个问题如何解决呢?我们可以引入版本号机制,只有版本号相同的时候才能进行替换操作
当舍友给你转账的时候,由于数值发生了变化,版本号也得到了修改
此时虽然我们A和V中的数值相同,但是版本号不同,所以无法进行交换
CAS的优缺点
优点
- 在并发量少或者对变量修改操作少的时候,效率会比传统的加锁高,因为不涉及用户态和内核态的切换。
缺点
- 自旋进行比较和替换,当并发量大的时候可能会因为变量一直更新而无法比较成功,而不断地进行自旋,导致CPU压力过大
- CAS只能保证一个变量的原子性,并不能保证整个代码块的原子性,所以在处理多个变量的原子性更新的时候还是得加锁。
- 上述的ABA问题,可以通过引入版本号解决
互斥锁 VS 自旋锁
互斥锁和自旋锁是最底层的两种锁,大部分的高级锁都是基于它们实现的,下面就来讲讲它们的区别
互斥锁
互斥锁是一种睡眠锁,即当一个线程占据了锁之后,其他加锁失败的线程就会进行睡眠
例如我们有A、B两个线程一同争抢互斥锁,当线程A成功抢到了互斥锁时,该锁就被他独占,在它释放锁之前,B的加锁操作就会失败,并且此时线程B将CPU让给其他线程,而自己则被阻塞。
对于互斥锁加锁失败后进入阻塞的现象,由操作系统的内核实现,如下图
- 当加锁失败时,内核会将线程置为睡眠状态,并将CPU切换给其他线程运行。此时从用户态切换至内核态
- 当锁被释放时,内核将线程至为就绪状态,然后在合适的时候唤醒线程获取锁,继续执行业务。此时从内核态切换至用户态
所以当互斥锁加锁失败的时候,就伴随着两次上下文切换的开销,而如果我们锁定的时间较短,可能上下文切换的时间会比锁定的时间还要长。
虽然互斥锁的使用难度较低,但是考虑到上下文切换的开销,在某些情况下我们还是会优先考虑自旋锁。
自旋锁
自旋锁是基于CAS实现的,它在用户态完成了加锁和解锁的操作,不会主动进行上下文的切换,因此它的开销相比于互斥锁也会少一些。
任何尝试获取该锁的线程都将一直进行尝试(即自旋),直到获得该锁,并且同一时间内只能由一个线程能够获得自旋锁。
自旋锁的本质其实就是对内存中一个整数的CAS操作,加锁包含以下步骤
- 查看整数的值,如果为0则说明锁空闲,则执行第二步,如果为1则说明锁忙碌,执行第三步
- 将整数的值设为1,当前线程进入临界区中
- 继续自旋检查(回到第一步),直到整数的值为0
从上面可以看出,对于获取自旋锁失败的线程会一直处于忙等待的情况,不断自旋直至获取锁资源,这也就要求我们必须要尽快释放锁,否则会占用大量的CPU资源
对比及应用场景
由于自旋锁和互斥锁的失败策略不同,自旋锁采用忙等待的策略,而互斥锁采用线程切换的策略,由于策略不同,它们的应用场景也不同。
由于自旋锁不需要进行线程切换,所以它完全在用户态下实现,加锁开销低,但是由于其采用忙等待的策略,对于短期加锁来说没问题,但是长期锁定的时候就会导致CPU资源的大量消耗。并且由于它不会睡眠,所以它可以应用于中断处理程序中。
互斥锁采用线程切换的策略,当切换到别的线程的时候,原线程就会进入睡眠(阻塞)状态,所以如果对睡眠有要求的情况可以考虑使用互斥锁。并且由于睡眠不会占用CPU资源,在长期加锁中它比起自旋锁有极大的优势
具体的应用场景如下表格所示
需求 | 加锁方式 |
---|---|
低开销加锁 | 自旋锁 |
短期锁定 | 自旋锁 |
长期锁定 | 互斥锁 |
中断上下文中加锁 | 自旋锁 |
持有锁需要睡眠 | 互斥锁 |
读写锁
读写锁用于明确区分读操作和写操作的场景。
其核心在于写独占,读共享
- 读锁是一个共享锁,当没有线程持有写锁的时候,读锁就可以被多个线程并发的持有,大大的提高了共享资源的访问效率。由于读锁只具备读权限,因此不存在线程安全问题。
- 写锁是一个独占锁(排他锁),当有任何一个线程持有写锁的时候,其余线程获取读锁和写锁的操作都会被阻塞
如下图
读锁 | 写锁 | |
---|---|---|
读锁 | 兼容 | 不兼容 |
写锁 | 不兼容 | 不兼容 |
实现方式
根据实现方式的不同,读写锁又分为读者优先、写者优先、读写公平
读者优先
读者优先期望的是读锁能够被更多的线程持有,以提高读线程的并发性。
为了做到这一点,它的规则如下:即使有线程申请了写锁,但是只要还有读者在读取内容,就允许其他的读线程继续申请读锁,而将申请写锁的进程阻塞,直到没有读线程在读时,才允许该线程写
流程如下图
写者优先
而写者优先则是优先服务于写进程
假设此时有读线程已经持有读锁,正在读,而另一写线程申请了写锁,写线程被阻塞。为了能够保证写者优先,此时后来的读线程获取读锁时则会被阻塞。而当先前的读线程释放读锁时,写线程则进行写操作,直到写线程写完之前,其他的线程都会被阻塞。
流程如下图
读写公平
从上面两个规则可以看出,读写优先都会导致另一方饥饿
- 读者优先时,对于读进程并发性高,但是如果一直有都进程获取读锁,就会导致写进程永远获取不到写锁,此时就会导致写进程饥饿。
- 写者优先时,虽然可以保证写进程不会饿死,但是如果一直有写进程获取写锁,导致读进程永远获取不到读锁,此时就会导致读进程饥饿。
既然偏袒哪一方都会导致另一方被饿死,所以我们可以搞一个读写公平的规则
实现方式:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁,这也读线程仍然可以并发,也不会出现饥饿的情况。
读写锁 VS 互斥锁
性能方面来说,读写锁的效率并不比互斥锁高。读锁加锁的开销并不比互斥锁小,因为它要实时维护当前读者的数量,在临界区很小,锁竞争不激烈的情况下,互斥锁的效率往往更快
虽然读写锁在速度上可能不如互斥锁,但是并发性好,对于并发要求高的地方,应该优先考虑读写锁。
Linux 线程安全常用的锁机制相关推荐
- Java基础-Java中常用的锁机制与使用
Java基础-Java中常用的锁机制与使用 锁lock或互斥mutex是一种同步机制,主要用于在存在多线程的环境中强制对资源进行访问限制.锁的主要作用为强制实施互斥排他以及并发控制策略.锁一般需要硬件 ...
- Java多线程(五) —— 线程并发库之锁机制
参考文献: http://www.blogjava.net/xylz/archive/2010/07/08/325587.html 一.Lock与ReentrantLock 前面的章节主要谈谈原子操作 ...
- Linux线程同步(二)---互斥锁实现线程同步
一 why 先给自己打个广告,本人的微信公众号:嵌入式Linux江湖,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题. 在博客&l ...
- Java中常用的锁机制
1.1什么是锁? 在计算机科学中,锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制.锁旨在强制实施互斥排他.并发控制策略. 锁通常需要硬件支持才能有效 ...
- linux线程同步之互斥锁——linux的关键区域
在windows中,为了让多个线程达到同步的目的,在对于全局变量等大家都要用的资源的使用上,通常得保证同时只能由一个线程在用,一个线程没有宣布对它的释放之前,不能够给其他线程使用这个变量.在windo ...
- Linux线程同步(三)---互斥锁源码分析
先给自己打个广告,本人的微信公众号:嵌入式Linux江湖,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题. 一 源码分析 1.li ...
- 读写自旋锁 linux,boost是否像Linux一样提供读写自旋锁机制?
用户态 spinlock 这个东西,其实没多大用.(其实在内核态用的地方也很少,基本上依赖 scheduler 的代码都不能用.) 如果你看过 Linux kernel 代码,你会知道在 disabl ...
- 【Linux 线程】常用线程函数复习《一》
1.pthread_create以及pthread_self函数 1 /**************************************************************** ...
- linux线程同步(4)-自旋锁
自旋锁与互斥量功能一样,唯一一点不同的就是互斥量阻塞后休眠让出cpu,而自旋锁阻塞后不会让出cpu,会一直忙等待,直到得到锁!!! 自旋锁在用户态使用的比较少,在内核使用的比较多!自旋锁的使用场景:锁 ...
最新文章
- Android中Parcel的分析和使用
- 唐朔飞计算机组成原理pdf_唐朔飞计算机组成原理第2版配套题库名校考研真题课后答案资料课后习题章节题库模拟试题...
- phone clone android,Phone Clone
- JS Map 简单实现
- Java中的初始化顺序
- Spring Cloud【Finchley】-13 Eureka Server HA高可用 2个/3个节点的搭建及服务注册调用
- NetBeans IDE 6.10 M1 发布
- 牛客网matlab怎么输入数据,从牛客上找的面经
- JAVA day06 继承,super,方法的重写,抽象类
- storm-starter 例子学习
- Cocos2dx使用 TexturePacker导出的.plist
- 家庭安防监控设备搭建
- 如何利用Camtasia为视频添加注释?
- Codeforces Round #364 (Div. 1) 700B(树)
- 区间最值操作与历史最值问题(二)
- 万字长文!用文本挖掘深度剖析54万首诗歌
- Power BI应用案例:淘宝用户行为分析实战
- WiFi 基础(四)—— 连接过程
- 怎么用科学计算机算反三角函数值域,反三角函数值域怎么求
- 阿里巴巴为新项目收购两枚双拼域名?
热门文章
- java后端参数默认值添加枚举_利用自定义Validator和枚举类来限定接口的入参
- mysql创建gbk库_MYSQL创建utf-8和GBK格式数据库
- 同步助手iphone4_88 元淘来的 iPhone 4 降级到 iOS 6,甚至还能跑 “大型游戏”
- java string类型时间比较大小吗_Java String类型时间比较大小
- android id 重名_android - 解决“应用自定义权限重名”
- 数据分发服务器管理系统,地图差分数据分发系统、地图差分数据分发装置、地图数据保有装置、更新管理服务器、以及地图差分提取服务器...
- jsp+servlet+mysql实现的图书管理系统源码
- 实验十:图形用户界面设计
- Mysql Update 流程摘抄
- synchronized 详解,偏向锁,轻量锁,重量锁