乐观锁与悲观锁讲解,CAS、synchronized、锁升级、ReentrantLock、AQS

什么是乐观锁和悲观锁

乐观锁:
总是假设最好的情况,每次拿数据的是时候都认为别人没有进行修改,所以不会加锁,但是为了保证线程安全,每次修改的时候都会判断这个数据有没有被修改过,适用于写少的场景,因为在写操作较多时如果失败会不断通过自旋判断数据有没有被修改,十分消耗CPU资源,CAS是实现乐观锁的一种方式,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

悲观锁:
总是假设最坏的情况,每次拿数据的时候都认为别人会修改数据,所以每次都要加锁,这样别人在拿这个数据的时候都会被阻塞,直到获取到锁 (共享资源每次只能由一个线程使用,使用的时候会加锁,别的线程会被阻塞,直到该线程释放锁后去尝试获取锁)适用于写多的场景,资源开销较小,Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁的实现

CAS

CAS是实现乐观锁的一种方式,即compare and swap(比较与交换),涉及
三个操作数:

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

会先把要原值A查询出来,然后只有当V=A时才会去写入新值B,如果不相等则说明原值已被别的线程修改过了,就会去不断自旋重试再次修改值

CAS的缺点

1、ABA问题

简单来说就是,当一个线程使用CAS时发现V=A,就一定说明这个值没有被别的线程修改过吗?不一定,因为可能其他线程修改了以后又被修改回来了,但是CAS还是会误认为这个值没有被修改过,这就是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没变,就写成了自己要改的值
那么要如何解决ABA问题呢?

加标志位,比如加一个自增的字段,在线程修改值的时候,这个值的自增标志位也加1,这时就算再把值修改回来后,标志位也不是一开始的那个了,在CAS的时候也判断一下标志位是否和一开始的标志位相等,这样就可以保证这个值一定是没有被其他线程修改过了
JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2、CPU开销大

如果CAS长时间都没有成功,就会一直自旋,给CPU带来了大量的执行开销

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

CAS只能保证单个共享变量的原子性
但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

悲观锁的实现

synchronized

synchronized是悲观锁的一种实现方式,synchronized的三种形式:

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的 Class 对象。
  • 对于同步方法块,锁是 Synchonized 括号里配置的对象。

synchronized 用的锁是存在 Java 对象头里的。如果对象是数组类型,则虚拟机用 3个字宽(Word)存储对象头,如果对象是非数组类型,则用 2 字宽存储对象头

对象头的结构:
在这里插入图片描述
在这里插入图片描述
32位虚拟机的Mark Word状态变化:
在这里插入图片描述
64位虚拟机的Mark Word状态变化:

在这里插入图片描述

锁升级在这里插入图片描述

在这里插入图片描述
偏向锁:
当一个线程访问同步快并获得锁的时候,会在对象头和栈帧的锁中记录偏向的线程ID,以后每次同一个线程访问这个同步块的时候就不需要加锁和解锁了,只需要看看对象头是否存储这个线程的偏向锁即可
偏向锁的撤销:
当其他线程想要竞争偏向锁的时候,会先暂停拥有偏向锁的线程并检查是否还活着

  • 如果没有活着,就把对象头设置成无锁状态,锁不升级,还是偏向锁,使用CAS替换偏向锁线程ID为另一个线程
  • 如果还活着,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

偏向锁的关闭:
-XX:- UseBiasedLocking=false,关闭偏向锁,程序默认会进入轻量级锁状态
轻量级锁:
当多个线程访问同步块的时候,会竞争锁,当有一个线程竞争到锁的时候,锁被替换为轻量级锁,其他线程通过自旋的方式不断获取锁,当自旋次数过大的时候,轻量级锁会升级为重量级锁,此时所有没有竞争到锁的线程会进入到阻塞队列中,锁被释放之后就会唤醒阻塞队列中的所有线程,并开始重新竞争锁
轻量级锁解锁:
轻量级解锁时,会使用原子的 CAS 操作将 Displaced Mark Word 替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。因为自旋很消耗cpu,所以当升级为重量级锁的时候就不会再恢复成轻量级锁了
重量级锁:
轻量级锁自旋次数过大时会升级为重量级锁,此时没有竞争到锁的线程会进入阻塞队列,等锁释放之后cpu会唤醒阻塞队列中的所有线程,重新竞争锁
锁的优缺点对比:
在这里插入图片描述

ReentrantLock

ReentrantLock可重入锁,在jdk1.7的ConcurrentHashMap中,就是由ReentrantLock实现的。

AQS

AQS是一个队列同步器,同步队列是一个双向链表,有一个状态标志位state,如果state为1的时候,表示有线程占用,其他线程会进入同步队列等待,如果有的线程需要等待一个条件,会进入等待队列,当满足这个条件的时候才进入同步队列,ReentrantLock就是基于AQS实现的
在这里插入图片描述

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值