前言

关于线程安全一提到可能就是加锁,在面试中也是面试官百问不厌的考察点,往往能看出面试者的基本功和是否对线程安全有自己的思考。

那锁本身是怎么去实现的呢?又有哪些加锁的方式呢?

我今天就简单聊一下乐观锁和悲观锁,他们对应的实现 CAS ,Synchronized,ReentrantLock

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

CAS 是怎么实现线程安全的?

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

举个栗子:现在一个线程要修改数据库的name,修改前我会先去数据库查name的值,发现name=“帅丙”,拿到值了,我们准备修改成name=“三歪”,在修改之前我们判断一下,原来的name是不是等于“帅丙”,如果被其他线程修改就会发现name不等于“帅丙”,我们就不进行操作,如果原来的值还是帅丙,我们就把name修改为“三歪”,至此,一个流程就结束了。

有点懵?理一下停下来理一下思路。

Tip:比较+更新 整体是一个原子操作,当然这个流程还是有问题的,我下面会提到。

他是乐观锁的一种实现,就是说认为数据总是不会被更改,我是乐观的仔,每次我都觉得你不会渣我,差不多是这个意思。

你这个栗子不错,他存在什么问题呢?

有,当然是有问题的,我也刚好想提到。

你们看图发现没,要是结果一直就一直循环了,CUP开销是个问题,还有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没变,就写成了自己要改的值

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

循环时间长开销大的问题

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

只能保证一个共享变量的原子操作

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

我还记得你之前说在JUC包下的原子类也是通过这个实现的,能举个栗子么?

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

大概意思就是循环判断给定偏移量是否等于内存中的偏移量,直到成功才退出,看到do while的循环没。

乐观锁在项目开发中的实践,有么?

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

那开发过程中ABA你们是怎么保证的?

加标志位,例如搞个自增的字段,操作一次就自增加一,或者搞个时间戳,比较时间戳的值。

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

之前不能防止ABA的正常修改:

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

带版本号能防止ABA的修改:

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

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

聊一下悲观锁?

悲观锁从宏观的角度讲就是,他是个渣男,你认为他每次都会渣你,所以你每次都提防着他。

我们先聊下JVM层面的synchronized:

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

它是如何保证同一时刻只有一个线程可以进入临界区呢?

synchronized,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。

我分别从他对对象、方法和代码块三方面加锁,去介绍他怎么保证线程安全的:

  • synchronized 对对象进行加锁,在 JVM 中,对象在内存中分为三块区域:对象头(Header)、实例数据(Instance
    Data)和对齐填充(Padding)。

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

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

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

    另外 Monitor 中还有两个队列分别是EntryList和WaitList,主要是用来存放进入及等待获取锁的线程。

    如果线程进入,则得到当前对象锁,那么别的线程在该类所有对象上的任何操作都不能进行。

    • Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

    • Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

在对象级使用锁通常是一种比较粗糙的方法,为什么要将整个对象都上锁,而不允许其他线程短暂地使用对象中其他同步方法来访问共享资源?

如果一个对象拥有多个资源,就不需要只为了让一个线程使用其中一部分资源,就将所有线程都锁在外面。

由于每个对象都有锁,可以如下所示使用虚拟对象来上锁:

   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 标志来实现的。

    我反编译了一小段代码,我们可以看一下我加锁了一个方法,在字节码长啥样,flags字段瞩目:

    synchronized void test();descriptor: ()Vflags: 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。

    同样看一下反编译后的一段锁定代码块的结果:

    public void syncTask();descriptor: ()Vflags: ACC_PUBLICCode:stack=3, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter  //注意此处,进入同步方法4: aload_05: dup6: getfield      #2             // Field i:I9: iconst_110: iadd11: putfield      #2            // Field i:I14: aload_115: monitorexit   //注意此处,退出同步方法16: goto          2419: astore_220: aload_121: monitorexit //注意此处,退出同步方法22: aload_223: athrow24: returnException table://省略其他字节码.......
    

小结:

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

两者的区别:同步方式是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现,同步代码块是通过monitorenter和monitorexit来实现。

我们知道了每个对象都与一个monitor相关联,而monitor可以被线程拥有或释放。

【深度解析】Java中的乐观锁、悲观锁相关推荐

  1. 深度解析Java中的5个“黑魔法”

    现在的编程语言越来越复杂,尽管有大量的文档和书籍,这些学习资料仍然只能描述编程语言的冰山一角.而这些编程语言中的很多功能,可能被永远隐藏在黑暗角落.本文将为你解释其中5个Java中隐藏的秘密,可以称其 ...

  2. 深度解析Java中的Comparable接口和Comparator接口

    大家好,我是小鱼儿 新的一天,大家一起加油! 目录 引子 Comparable接口 Comparator接口 引子 我们之前的文章的文章提到了Arrays是一个数组工具类,用Arrays.sort能够 ...

  3. java 对变量加锁_Java最全锁剖析:独享锁/共享锁+公平锁/非公平锁+乐观锁/悲观锁...

    乐观锁 VS 悲观锁 乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度,在Java和数据库中都有此概念对应的实际应用. 1.乐观锁 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会 ...

  4. Java锁详解:“独享锁/共享锁+公平锁/非公平锁+乐观锁/悲观锁+线程锁”

    在Java并发场景中,会涉及到各种各样的锁如公平锁,乐观锁,悲观锁等等,这篇文章介绍各种锁的分类: 公平锁/非公平锁 可重入锁 独享锁/共享锁 乐观锁/悲观锁 分段锁 自旋锁 线程锁 乐观锁 VS 悲 ...

  5. 最全Java锁详解:独享锁/共享锁+公平锁/非公平锁+乐观锁/悲观锁

    在Java并发场景中,会涉及到各种各样的锁,比如:高并发编程系列:4种常用Java线程锁的特点,性能比较.使用场景,这些锁有对应的种类:公平锁,乐观锁,悲观锁等等,这篇文章来详细介绍各种锁的分类: 公 ...

  6. **Java有哪些悲观锁的实现_「Java并发编程」何谓悲观锁与乐观锁,Java编程你会吗...

    何谓悲观锁与乐观锁 悲观锁 乐观锁 两种锁的使用场景 乐观锁常见的两种实现方式 1. 版本号机制 2. CAS算法 乐观锁的缺点 1 ABA 问题 2 循环时间长开销大 3 只能保证一个共享变量的原子 ...

  7. Java并发编程(05):悲观锁和乐观锁机制

    本文源码:GitHub·点这里 || GitEE·点这里 一.资源和加锁 1.场景描述 多线程并发访问同一个资源问题,假如线程A获取变量之后修改变量值,线程C在此时也获取变量值并且修改,两个线程同时并 ...

  8. Java 面试 :乐观锁 悲观锁

    乐观锁悲观锁,是为了解决多线程并发操作共享变量可能导致的脏读.幻读和不可重复读等问题 悲观锁 悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式.总是假设最坏的情况,每次读取数据的时候都默认 ...

  9. Java 乐观锁 悲观锁

    一.乐观锁.悲观锁定义 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据.因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行 ...

  10. mysql默认乐观锁悲观锁_MySQL中悲观锁和乐观锁到底是什么?-阿里云开发者社区...

    索引和锁是数据库中的两个核心知识点,隔离级别的实现都是通过锁来完成的 按照锁颗粒对锁进行划分 ? 锁用来对数据进行锁定,我们可以从锁定对象的粒度大小来对锁进行划分,分别为行锁.页锁和表锁. 行锁就是按 ...

最新文章

  1. python 使用socks5 设置全局代理
  2. springboot多模块项目,无法找到另一个模块的实体类
  3. GridView的多主键(Key)取值问题
  4. 1 理解Linux系统的“平均负载”
  5. centos Error: Cannot find a valid baseurl for repo: base 解决方法
  6. 1159: 零起点学算法66——反话连篇
  7. 一加7T系列发布时间公布:9月26日北美和印度率先亮相
  8. NAME:WRECK 漏洞影响近亿台物联网设备
  9. noip2012借教室
  10. Springboot整合junit单元测试
  11. 计算机考研数据结构代码题总结--Day01
  12. SketchUp 更新插件,不用重启让更新生效
  13. 瑞星网络版防病毒软件For Linux通过银河麒麟产品兼容性测试
  14. python绘制正态分布图_在python中画正态分布图像的实例
  15. c语言中(char)的用法,c语言中char的用法是什么意思.docx
  16. ECCV 2020预会议 直播笔记| Cross-Modal Weighting Network for RGB-D Salient Object Detection
  17. XZK-JAVA-支线任务-010605-MVC,登陆案例
  18. 【复习笔记】计算机网络求职考点整理
  19. GOTC 2023全球开源技术峰会
  20. Java字符串反转函数reverse()

热门文章

  1. 获取应用程序实例句柄HINSTANCE
  2. 最近特别火的给Emoji表情加小辫子,双端通用方法来了
  3. 云原生控制平面项目Crossplane发布1.0版本|定义你自己的云平台
  4. 开心测试卷答案软件六下外研版,外研版六下英语Module10达标检测卷含答案
  5. 海底磁异常条带研究综述及南海重建
  6. 沉痛哀悼念 相声名家张文顺先生
  7. 交换网络安全技术,端口隔离的实现和配置,网络工程师实战分享
  8. DM8 十大新特性:
  9. kettle一闪而过的问题:
  10. MySQL百万数据优化总结 一