本文参考——敖丙——JavaFamily

概述

上一篇文章我们提到了乐观锁和悲观锁的工作方式和使用场景,那么这两种锁本身是如何实现的?

这篇文章就来总结一下乐观锁和悲观锁,他们对应的实现—— CAS ,Synchronized,ReentrantLock

乐观锁——CAS

什么是CAS
CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。

CAS是如何保证线程安全的?
线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。

Tips:这就是Compare and Swap的由来,注意比较-替换是一个原子操作,要么两个同时完成,要么两个同时失败。

它总是乐观的去考虑事件,认为线程发生冲突的概率很小

那么CAS就这么完美吗?它存在什么问题?
世界上从来就没有完美的东西,CAS也一样,我们可以看上面的流程图,如果数据值一直发送改变,那么CAS就会一直自旋,CPU开销就是一个问题,而且CAS还有一个经典的ABA问题

什么是ABA问题?

我们来捋一下上图的工作流程
1.线程1读取了数据A
2.线程2读取了数据A
3.线程2通过CAS比较,发现数据是A没错,修改数据A为B
4.线程3读取数据B
5.线程3通过CAS比较,发现是数据B没错,修改数据B为A
6.线程1通过CAS比较,发现是数据A没错,修改数据A为B

在这个过程中任何线程都没做错什么,但是值被改变了,线程1却没有办法发现,其实这样的情况出现对结果本身是没有什么影响的,但是我们还是要防范,怎么防范我下面会提到。

如何防止ABA问题发生
加标志位,例如搞个自增的字段(版本号之类),操作一次就自增加一,或者搞个时间戳,比较时间戳的值。

举个栗子:现在我们去要求操作数据库,根据CAS的原则我们本来只需要查询原本的值就好了,现在我们一同查出他的标志位版本字段vision。

只查询原本的值不能防止ABA

update table set value = newValue where value = #{oldValue}
//oldValue就是我们执行前查询出来的值

加上标志位version之后

update table set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision}
// 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样

除了版本号,像什么时间戳,还有JUC工具包里面也提供了这样的类,想要扩展的小伙伴可以去了解一下。

CPU忙等待时间过长导致的开销问题
是因为CAS操作长时间不成功的话,会导致一直自旋,相当于死循环了,CPU的压力会很大。

CAS可以操作多个共享变量吗
CAS操作单个共享变量的时候可以保证原子的操作,多个变量就不行了,JDK 5之后 AtomicReference可以用来保证对象之间的原子性,就可以把多个对象放入CAS中操作。

就拿AtomicInteger举例,他的自增函数incrementAndGet()就是这样实现的,其中就有大量循环判断的过程,直到符合条件才成功。


乐观锁在项目开发中的实践?
比如我们在很多订单表,流水表,为了防止并发问题,就会加入CAS的校验过程,保证了线程的安全,但是看场景使用,并不是适用所有场景,他的优点缺点都很明显。

悲观锁——Synchronized和ReentrantLock

悲观锁就是考虑事情比较悲观,认为在访问共享资源时发生冲突的概率比较高,所以每次访问前线程都要先加锁。

我们首先来聊一下基于JVM层面实现的synchronized关键字

synchronized加锁,synchronized 是最常用的线程同步手段之一,上面提到的CAS是乐观锁的实现,synchronized就是悲观锁了。

它是如何保证同一时刻只有一个线程可以进入临界区呢?
使用synchronized对一个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。

我们分别从方法,代码块,对象三个层面解析它的实现原理,以及它是如何保证线程安全的

  • synchronized对对象进行加锁,在JVM中,每个对象可以分为三个区域,对象头,实例数据和对齐填充。每个对象都有一把锁,存放在对象头中

    • 对象头:我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

      • Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
      • Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

我们可以看到对象头中保存了锁标志位和指向 monitor 对象的起始地址,如下图所示,右侧就是对象对应的 Monitor 对象。

当 Monitor 被某个线程持有后,就会处于锁定状态,(如图中的 Owner 部分,)会指向持有 Monitor 对象的线程。

Monitor对象中还有两个队列EntryList和WaitList,Entry队列主要是存放还在等待锁的线程,而Wait队列存放那些已经运行完并释放锁的线程。

当一个线程获取到这个对象的锁之后,其他线程在该类的所有对象上的操作都不可以进行。

Tips:在对象层面使用锁是一种相对粗糙的方式,为什么我们要对整个对象上锁,不允许其他线程短暂地使用对象中的其他同步方法访问共享资源

就好比一个对象有多个共享资源,我们不需要为了一个线程使用其中的一部分资源,就将其他线程全部锁在外面

由于每个对象都有锁,我们可以使用虚拟对象来上锁

class FineGrainLock{MyMemberClassx,y;Object xlock = new Object(), ylock = newObject();public void foo(){synchronized(xlock){//accessxhere}//dosomethinghere-butdon'tusesharedresourcessynchronized(ylock){//accessyhere}}public void bar(){synchronized(this){//accessbothxandyhere}//dosomethinghere-butdon'tusesharedresources}}
  • synchronized应用在方法上时,在字节码中是通过标志位ACC_SYNCHRONIZED来实现的
synchronized void test();descriptor: ()V//拥有这个标志位就表明当前锁已经被持有flags: ACC_SYNCHRONIZEDCode:stack=0, locals=1, args_size=10: returnLineNumberTable:line 7: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       1     0  this   Ljvm/ClassCompile;

其他线程进这个方法就看看是否有这个标志位,有就代表有别的仔拥有了他,你就别碰了。

  • synchronized应用在代码块上时,在字节码中是通过monitorenter和monitorexit这两条指令实现的

每个对象都会与一个monitor相关联,当某个monitor被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor。

工作流程
1.每个monitor维护着一个记录着拥有次数的计数器。未被拥有的monitor的该计数器为0,当一个线程获得monitor(执行monitorenter)后,该计数器自增变为 1 。

  • 当同一个线程再次获得monitor对象时,计数器自增(可重入)
  • 当不同线程想要获取该monitor对象会被阻塞
    2.当同一个线程释放monitor(执行monitorexit指令),计数器自减,当计数器变为0的时候,释放monitor对象,这时候其他线程可以获取这个monitor了

小结

同步方法和代码块都是底层通过monitor来实现的。

两者的区别在于同步方法是通过标志位ACC_SYNCHRONIZED来实现的,而同步代码块是通过执行monitorenter,monitorexit指令来实现的。

我们以前说synchronized是重量级锁,为什么现在不提了
在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。

但是,随着 Java SE 1.6 对 synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。

针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。

锁的状态有4种
无锁<偏向锁<轻量级锁<重量级锁

锁只能升级,不能降级

ReentrantLock

在介绍ReentrantLock之前,我们有必要先了解AQS(AbstractQueuedSynchronizer)

AQS:也就是队列同步器,这是实现 ReentrantLock 的基础。

AQS有一个标志位state,值为1时表示已经有线程占用,其他线程需要进入同步队列等待,这里说的同步队列其实是一个双向链表
当获得锁的线程还需要等待某个条件时,就转移到condition的等待队列,等待队列可以有多个。

当 condition 条件满足时,线程会从等待队列重新进入同步队列进行获取锁的竞争。

Tips:等待队列就是除了锁之外还需要等待别的条件,而同步队列就只需要等待锁释放然后再去和别的线程竞争即可

ReentrantLock 就是基于 AQS 实现的,如下图所示,ReentrantLock 内部有公平锁和非公平锁两种实现,差别就在于新来的线程是否比已经在同步队列中的等待线程更早获得锁。

和 ReentrantLock 实现方式类似,Semaphore 也是基于 AQS 的,差别在于 ReentrantLock 是独占锁,Semaphore 是共享锁。

从上图可以看出,ReentrantLock有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。

Sync有公平锁FairSync和非公平锁NotFairSync两个子类

ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。

总结

锁其实还有很多,例如自旋锁,自适应自旋,公平锁,非公平锁,可重入(文中提到的都是可重入),不可重入锁,共享锁,排他锁等。希望以后可以弄清楚它们的原理并自己总结出来。

乐观锁和悲观锁的底层实现原理相关推荐

  1. 一篇文章带你弄懂乐观锁与悲观锁,CAS原子性,synchronized底层原理

    文中加入了个人理解,如有不准确的地方欢迎提出,笔者会及时的进行改正. 乐观锁与悲观锁 乐观锁: 假设数据不会发生冲突,只有在进行数据更新的才会对数据进行检查,如果冲突则更新失败并返回错误信息 悲观锁: ...

  2. **Java有哪些悲观锁的实现_面试4连问:乐观锁与悲观锁的概念、实现方式、场景、优缺点?...

    推荐阅读: 数据库面试4连问:分库分表,中间件,优缺点,如何拆分? 终极手撕之架构大全:分布式+框架+微服务+性能优化,够不够? 消息队列面试,你能顶得住面试官这波10大连环炮的攻势吗? 01 乐观锁 ...

  3. 悲观锁和乐观锁_乐观锁和悲观锁 以及 乐观锁的一种实现方式-CAS

    悲观锁 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞知道它拿到锁.传统的关系型数据库里面就用到了很多的这种锁机制,比如行锁,表锁等 ...

  4. 乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

    首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.传统的关系型数据库里边就用到了很 ...

  5. 详解各种锁:CAS、共享锁、排它锁、互斥锁、悲观锁、乐观锁、行级锁、表级锁、页级锁、死锁、JAVA对CAS的支持、ABA问题、AQS原理

    共享锁(S锁) 又称为读锁,可以查看但无法修改和删除的一种数据锁.如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁.获准共享锁的事务只能读数据,不能修改数据. 共享锁下其它用 ...

  6. innodb 悲观锁 乐观锁_mysql乐观锁、悲观锁、共享锁、排它锁、行锁、表锁

    mysql乐观锁.悲观锁.共享锁.排它锁.行锁.表锁 乐观锁 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使 ...

  7. 悲观锁和乐观锁_面试必备之乐观锁与悲观锁

    何谓悲观锁与乐观锁 乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展.这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人. 大家可以点 ...

  8. Java并发篇_乐观锁与悲观锁

    乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展. 一.引入概念 1.悲观锁 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次 ...

  9. Java多线程学习总结(5)——乐观锁和悲观锁的基本概念、实现方式(含实例)、适用场景及常见面试题

     分享一个大神的人工智能教程.零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到人工智能的队伍中来!点击浏览教程 一.基本概念 乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题. 乐观锁 ...

最新文章

  1. 2008R2文件服务器迁移到2012R2
  2. 学习:Java泛型之一
  3. C++中auto的用法,说明的是变量的寿命
  4. H5版定点投篮游戏编程设计--物理模型抽象
  5. Jmail的使用,可以发送给多人
  6. C语言用循环结构算平均值,C语言循环结构选择题().doc
  7. 7个顶级资源网站,不知道就很可惜
  8. 深度操作系统 Deepin 20 BETA 发布
  9. 创业者都有一个共同的特质,他想做事,就立刻做
  10. Java库转oc_急急急!各位大神:一段JAVA代码转成OC代码。
  11. 视频编解码质量评价---BDBR与BD-PSNR
  12. MySQL数据类型 int(M) 表示什么意思?详解mysql int类型的长度值问题
  13. 字典写入excel_Excel中“先出式”出货的问题,以后出库太方便了
  14. 求一元二次方程的解法c语言,一元二次方程的解法(全)
  15. 微软服务器2016各版本区别,Win10各版本区别 最强的你绝对没猜到
  16. 预防建筑倒塌高还能这样?
  17. Win10系统内置杀毒软件Windows Defender卸载方法
  18. 第4章【思考与练习2】数据文件high-speed rail.csv存放着世界各国高速铁路的情况。对世界各国高铁的数据进行绘图分析。使用Basemap绘制地图及使用Pyecharts绘制地图。
  19. 永磁同步电机力矩控制(二):FOC与DTC
  20. 本地ecshop网站怎么上传到服务器,ecshop 上传服务器

热门文章

  1. 四个月大数据自学进大厂总结
  2. 20计算机考研,推荐哪些学校?
  3. Python爬虫培训或学习过程中,需掌握的爬取验证码方法
  4. 一种高选择性和灵敏的荧光生物标记物,可用于标记碱性磷酸酶 (ALP),5-FAM-Alkyne,510758-19-7,荧光生物标记物
  5. 凯文·凯利《给年轻人的99条建议》2021
  6. css 调字符间距和段首缩进
  7. 2021-07-25 论代码质量与程序员
  8. MegaCli 安装和基本使用
  9. WIN2K 实用软件
  10. 抗衰老药物试用成功!永葆年轻即将成真?