文章目录

  • 乐观锁 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操作,加锁包含以下步骤

  1. 查看整数的值,如果为0则说明锁空闲,则执行第二步,如果为1则说明锁忙碌,执行第三步
  2. 将整数的值设为1,当前线程进入临界区中
  3. 继续自旋检查(回到第一步),直到整数的值为0

从上面可以看出,对于获取自旋锁失败的线程会一直处于忙等待的情况,不断自旋直至获取锁资源,这也就要求我们必须要尽快释放锁,否则会占用大量的CPU资源


对比及应用场景

由于自旋锁和互斥锁的失败策略不同,自旋锁采用忙等待的策略,而互斥锁采用线程切换的策略,由于策略不同,它们的应用场景也不同。

由于自旋锁不需要进行线程切换,所以它完全在用户态下实现,加锁开销低,但是由于其采用忙等待的策略,对于短期加锁来说没问题,但是长期锁定的时候就会导致CPU资源的大量消耗。并且由于它不会睡眠,所以它可以应用于中断处理程序中。

互斥锁采用线程切换的策略,当切换到别的线程的时候,原线程就会进入睡眠(阻塞)状态,所以如果对睡眠有要求的情况可以考虑使用互斥锁。并且由于睡眠不会占用CPU资源,在长期加锁中它比起自旋锁有极大的优势

具体的应用场景如下表格所示

需求 加锁方式
低开销加锁 自旋锁
短期锁定 自旋锁
长期锁定 互斥锁
中断上下文中加锁 自旋锁
持有锁需要睡眠 互斥锁

读写锁

读写锁用于明确区分读操作和写操作的场景

其核心在于写独占,读共享

  • 读锁是一个共享锁,当没有线程持有写锁的时候,读锁就可以被多个线程并发的持有,大大的提高了共享资源的访问效率。由于读锁只具备读权限,因此不存在线程安全问题。
  • 写锁是一个独占锁(排他锁),当有任何一个线程持有写锁的时候,其余线程获取读锁和写锁的操作都会被阻塞

如下图

读锁 写锁
读锁 兼容 不兼容
写锁 不兼容 不兼容

实现方式

根据实现方式的不同,读写锁又分为读者优先、写者优先、读写公平

读者优先

读者优先期望的是读锁能够被更多的线程持有,以提高读线程的并发性。

为了做到这一点,它的规则如下:即使有线程申请了写锁,但是只要还有读者在读取内容,就允许其他的读线程继续申请读锁,而将申请写锁的进程阻塞,直到没有读线程在读时,才允许该线程写

流程如下图

写者优先

而写者优先则是优先服务于写进程

假设此时有读线程已经持有读锁,正在读,而另一写线程申请了写锁,写线程被阻塞。为了能够保证写者优先,此时后来的读线程获取读锁时则会被阻塞。而当先前的读线程释放读锁时,写线程则进行写操作,直到写线程写完之前,其他的线程都会被阻塞。

流程如下图

读写公平

从上面两个规则可以看出,读写优先都会导致另一方饥饿

  • 读者优先时,对于读进程并发性高,但是如果一直有都进程获取读锁,就会导致写进程永远获取不到写锁,此时就会导致写进程饥饿。
  • 写者优先时,虽然可以保证写进程不会饿死,但是如果一直有写进程获取写锁,导致读进程永远获取不到读锁,此时就会导致读进程饥饿。

既然偏袒哪一方都会导致另一方被饿死,所以我们可以搞一个读写公平的规则

实现方式:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁,这也读线程仍然可以并发,也不会出现饥饿的情况。


读写锁 VS 互斥锁

性能方面来说,读写锁的效率并不比互斥锁高。读锁加锁的开销并不比互斥锁小,因为它要实时维护当前读者的数量,在临界区很小,锁竞争不激烈的情况下,互斥锁的效率往往更快

虽然读写锁在速度上可能不如互斥锁,但是并发性好,对于并发要求高的地方,应该优先考虑读写锁。

并发编程中常见的锁机制:乐观锁、悲观锁、CAS、自旋锁、互斥锁、读写锁相关推荐

  1. 并发编程中的锁、条件变量和信号量

    在并发编程中,经常会涉及到锁.条件变量和信号量.本文从并发开始,探究为什么需要它们,它们的概念,实现原理以及应用. 并发简介 并发是指多个事情,在同一时间段内同时发生了.和并发经常一起被提到的是并行. ...

  2. Java中的锁机制 -- 乐观锁、悲观锁、自旋锁、可重入锁、读写锁、公平锁、非公平锁、共享锁、独占锁、重量级锁、轻量级锁、偏向锁、分段锁、互斥锁、同步锁、死锁、锁粗化、锁消除

    文章目录 1. Java中的锁机制 1.1 乐观锁 1.2 悲观锁 1.3 自旋锁 1.4 可重入锁(递归锁) 1.5 读写锁 1.6 公平锁 1.7 非公平锁 1.8 共享锁 1.9 独占锁 1.1 ...

  3. JUC里面的相关分类|| java并发编程中,关于锁的实现方式有两种synchronized ,Lock || Lock——ReentrantLock||AQS(抽象队列同步器)

    JUC分类 java并发编程中,关于锁的实现方式有两种synchronized ,Lock AQS--AbstractQueuedSynchronizer

  4. 并发编程中,你加的锁未必安全

    摘要:在编写多线程并发程序时,我明明对共享资源加锁了啊?为什么还是出问题呢?问题到底出在哪里呢?其实,我想说的是:你的加锁姿势正确吗? 本文分享自华为云社区<[高并发]高并发环境下诡异的加锁问题 ...

  5. Java并发编程中的若干核心技术,向高手进阶

    来源:http://www.jianshu.com/p/5f499f8212e7 引言 本文试图从一个更高的视角来总结Java语言中的并发编程内容,希望阅读完本文之后,可以收获一些内容,至少应该知道在 ...

  6. Go并发编程中的那些事[译]

    原文地址:Concurrent programming 原文作者:StefanNilsson 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:kobehah ...

  7. Java并发编程(8)——常见的线程安全问题

    线程安全问题: 多个线程同时执行也能工作的代码就是线程安全的代码 如果一段代码可以保证多个线程访问的时候正确操作共享数据,那么它是线程安全的. 具体说明: java并发线程实战(1) 线程安全和机制原 ...

  8. cas无法使用_并发编程中cas的这三大问题你知道吗?

    在java中cas真的无处不在,它的全名是compare and swap,即比较和交换.它不只是一种技术更是一种思想,让我们在并发编程中保证数据原子性,除了用锁之外还多了一种选择. 一.cas的思想 ...

  9. java内存栅栏_内存屏障(Memory Barriers/Fences) - 并发编程中最基础的一项技术

    我们经常都听到并发编程,但很多人都被其高大上的感觉迷惑而停留在知道听说这一层面,下面我们就来讨论并发编程中最基础的一项技术:内存屏障或内存栅栏,也就是让一个CPU处理单元中的内存状态对其它处理单元可见 ...

最新文章

  1. .net core发布 正在发现数据上下文_使用EF Core实现数据库读写分离
  2. 获取网页中的验证码图片
  3. BXP网卡换槽之后就要按“任意键”的问题解决方法!(转)
  4. 官方验证!雨林木风 Ghost XP SP3 装机版 ylmf_xp3_yn9.8 !!附:官方全部MD5!
  5. 从零学iFIX视频教程 2.01版 完整目录 (总共220节视频)
  6. vue生成静态html文件_Vue项目打包成一个HTML文件(包含CSS,JS)
  7. C编程入门到精通 F1: 学习本课程常见问题说明
  8. python 利用 Turtle库 画太阳花图形
  9. 优锘科技:物联森友会发布助物联网企业加速成长
  10. 【手机跳板 多款软件测试】图文演示!
  11. 程序存储器 指令寄存器 程序计数器 地址寄存器
  12. 大学英语综合教程四 Unit 1 课文内容英译中 中英翻译
  13. ESP8266红外学习遥控器
  14. 112-smart-toc-2021-09-09
  15. LDA + SVM 文本分类
  16. UE4 Sequence学习
  17. 《卓有成效的管理者》读后感
  18. 一个硕士是怎么样发5篇SCI的
  19. 取消选中单选框radio的三种方式
  20. ltrim用法(通俗易懂版)

热门文章

  1. 字典-字典和列表组合的应用场景
  2. SpringBoot高级-检索-Elasticsearch简介安装
  3. 适配器模式源码解析(jdk+spring+springjpa+springmvc)
  4. CyclicBarrier详解
  5. python中case的用法_python中Switch/Case实现的示例代码
  6. python判断两线段是否相交_c语言 判断两直线段是否相交
  7. 在 Java 中,为什么需要创建内部类对象之前需要先创建外部类对象
  8. Handler 源码解析(Java 层)
  9. 江苏:5G先行,智慧江苏再进一步
  10. 烟台农业走进物联网大数据时代