JavaEE——常见的锁策略、CAS、synchronized 原理(八股)

文章目录

  • 一、常见的锁策略
    • 1.乐观锁 & 悲观锁
    • 2.轻量级锁 & 重量级锁
    • 3.自旋锁 & 挂起等待锁
    • 4.互斥锁 & 读写锁
    • 5. 公平锁 & 非公平锁
  • 二、CAS
    • 1、什么是 CAS
    • 2. CAS 的应用场景
    • 2.实现自旋锁
    • 3. CAS 中的 ABA 问题
  • 三、 Synchronized 原理

一、常见的锁策略

当前此处介绍的这些所策略不仅仅局限于 Java,任何与锁相关的特性都与这些相关。

1.乐观锁 & 悲观锁

这里要注意的是,此处说的锁并不是单指一把锁,而是一类锁。

  • 乐观锁: 通常假设不会发生冲突,只有在数据提交更新时才会对冲突进行检测,如有冲突就会返回信息,交给用户处理。
  • 悲观锁: 通常假设最坏的情况,冲突一直存在,认为每次获取数据是都会有人修改,所以每次获取数据时都会加锁。

总的来讲,就是两者对锁的竞争激烈程度的认知不同。

synchronized 既是一个悲观锁,又是一个乐观锁。
默认是乐观锁,但是发现所竞争比较激烈时,就会变成悲观锁。

2.轻量级锁 & 重量级锁

  • 轻量级锁: 轻量级加锁的开销较小,效率更高。(进行了少量内核态用户切换)
  • 重量级锁: 重量级加锁的开销较大,效率更低。(进行了大量内核态用户切换)

这里说明一个比较常见的情况(多数情况下):
多数情况下,乐观锁,是一个轻量级锁。
悲观锁,是一个重量级锁。

synchronized 既是一个轻量级锁,又是一个重量级锁。
默认是轻量级锁,当锁竞争比较激烈时,就会转换为重量级锁。

3.自旋锁 & 挂起等待锁

首先说明一下这两类锁的特性;
自旋锁,是一种典型的轻量级锁。
挂起等待锁,是一种典型的重量级锁。

下面我通过举例来解释两类锁的特点:
首先设想一个场景,这里我们邀请三为人选,分别是: 喜羊羊,美羊羊,沸羊羊

此时,沸羊羊 向 美羊羊 表白(尝试对美羊羊加锁),但是,美羊羊说,沸羊羊你是个好人,但是我有男朋友了(说明此时 喜羊羊 对美羊羊已经加锁)

呢么,沸羊羊想要上位,就只能等待(锁释放),于是有下面两种。

  • 针对自旋锁: 每天都去问候美羊羊,时刻关心她,一但她和喜羊羊分手,可以第一时间得知。快速尝试获取锁,并建立关系。
    很明显,自旋锁的形式,占用了大量的资源。
  • 针对挂起等待锁: 沸羊羊被婉拒后,暗下决心,表示我愿意等,只要你幸福开心,我就躲起来不打扰你,如果哪一天想起我,你就告诉我。
    这样就非常不确定,当 美羊羊 分手了,可能想起来沸羊羊,求安慰。但是,大概率根本不会想到。
    所以对于挂起等待锁,当真正被唤醒的时候就可能已经沧海桑田了。但是优点也就是相对节省资源。

synchronized 这里的轻量级锁,是基于自旋锁的形式实现。
synchronized 这里的重量级锁,是基于挂起等待锁实现的。

4.互斥锁 & 读写锁

  • 互斥锁: 就是提供 加锁 和 解锁 两个操作。当一个线程加锁了,另一个线程也尝试加锁就会阻塞等待。
  • 读写锁: 提供三种操作
    1.针对读加锁
    2.针对写加锁
    3.解锁
    上面的三种操作,针对的是多线程对同一个变量并发写操作,读操作没有线程安全问题。
    即就是,读锁与读锁之间没有互斥,写锁与写锁之间存在互斥,写锁与读锁之间存在互斥

synchronized 不是读写锁

5. 公平锁 & 非公平锁

在这里,我们将公平定义为先来后到

同样,这里可以设定一个场景,此时假设美羊羊分手了。

公平锁: 当美羊羊分手后,由等待队列中最早的舔狗上位。(阻塞队列中的元素根据顺序依次获取锁)
JavaEE——常见的锁策略、CAS、synchronized 原理(八股)_第1张图片

  • 非公平锁: 三个人都不等了,开始上去争抢,各凭本事。(阻塞队列中的元素竞争获取锁)
    JavaEE——常见的锁策略、CAS、synchronized 原理(八股)_第2张图片

synchronized 是非公平锁。

二、CAS

1、什么是 CAS

CAS:全称为Compare and swap,字面意思: 比较并交换

假设内存中的数据 V,旧的预期值 A,需要修改的新值 B。

  1. 比较 A 和 V 的值是否相同。(比较)
  2. 若比较的值相同,将 B 写入 V。(交换)
  3. 返回操作是否成功。

如图所示:
JavaEE——常见的锁策略、CAS、synchronized 原理(八股)_第3张图片
这里需要注意的是,CAS 的这个过程,并非是一段代码实现,而是通过 一条 CPU 指令实现。
也就是说 CAS 操作是原子性的,这样就可以在一定程度上回避线程安全问题。

2. CAS 的应用场景

  1. 实现原子类

因为是原子类,真实的 CAS 是一个原子硬件指令来完成的,实现的是 i++ 这样的操作,这里只能使用伪代码来辅助理解,如图:
JavaEE——常见的锁策略、CAS、synchronized 原理(八股)_第4张图片
JavaEE——常见的锁策略、CAS、synchronized 原理(八股)_第5张图片
如上图所示,设定两个线程分别实现自增。
JavaEE——常见的锁策略、CAS、synchronized 原理(八股)_第6张图片
此时假设线程1 优先抢占CPU
JavaEE——常见的锁策略、CAS、synchronized 原理(八股)_第7张图片
此时,线程2进入 CPU
JavaEE——常见的锁策略、CAS、synchronized 原理(八股)_第8张图片
最后返回线程各自的 oldvalue 即可!

总的来说,CAS 就是 CPU 提供给我们的一种特殊指令,通过这个指令,可以再一定程度上处理线程安全问题。

2.实现自旋锁

同样,自旋锁也是以伪代码的形式展现。如图所示:
JavaEE——常见的锁策略、CAS、synchronized 原理(八股)_第9张图片

3. CAS 中的 ABA 问题

什么是 ABA 问题

我们已知,CAS 在运行中,就是检查 value 和 oldvalue 是否一致。如果一致,就视为 value 的值中途没有被修改过,所以下一步交换没有问题。

需要注意的是这里的 “一致”,可能是没有改过,也可能是 改过,但是又还原回来了。

通俗来讲,就是,我买了个手机,这个手机可能是新机,也可能是翻新机这里我们不专业,无法区分!

ABA 这样的问题,在大部分情况下影响都不大。但是,仍然有极端情况不容忽视,问题如下:
假设到 ATM 上取钱,在取钱的时候,按下取钱键的一瞬间,机器故障卡了,此时我又不耐烦的多按了几下,此时就可能产生 bug,造成重复扣款的情况,如图:

JavaEE——常见的锁策略、CAS、synchronized 原理(八股)_第10张图片

解决方案

给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.

  • CAS 操作在读取旧值的同时, 也要读取版本号.
  • 真正修改的时候,
    1.如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
    2.如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).

三、 Synchronized 原理

目前,我们知道 synchronized 的最基本用法是,两个线程针对同一个对象加锁,会产生阻塞等待。
对于 synchronized 内部,其实还有很多优化机制,存在的目的就是为了让锁更高效。

  1. 锁升级/锁膨胀
    当代码执行到 synchronized 代码块中实现加锁可能会经历下面几个过程,如图:
    JavaEE——常见的锁策略、CAS、synchronized 原理(八股)_第11张图片
  • 偏向锁: 进行加锁时,首先会进入到偏向锁这个状态。
    这里并不是真正的加锁,而是先占一个位置,如有需要就真加锁,无需要就放弃。
  • 轻量级锁: 当发生锁竞争的时候,就会从偏向锁升级为轻量级锁
    此时 synchronized 会通过 自旋 的方式进行加锁。
  • 重量级锁: 在 synchronized 进行自旋时,内部有个计数器,当自旋到一定时间后,就会自动升级成重量级锁。
    此时锁的类型是 挂起等待锁 ,是基于操作系统原生的 API 进行加锁了。

呢么有个问题,尽然能实现锁升级,呢么可不可以降级?
答案是不行。 在 JVM 的主流实现中,只有锁升级,没有锁降级。只要是锁对象,一旦被升级,就不能再回头了。

  1. 消除锁
    编译器的智能判定,看当前的代码是否真的需要加锁,如果不需要,但是程序员加了,就自动将锁消除。
  2. 锁粗化
    锁的粒度:synchronized 所包含的代码越多,粒度就越粗,包含的越少,粒度就越细。
    通常情况下,一般认为锁的粒度细一点较好。

对于这里的内容,本人整理的十分有限,不足的地方还希望大家多多指点。

你可能感兴趣的:(JavaEE,java-ee)