1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-16 18:10:13 +08:00

[docs update]完善 AQS 内容

This commit is contained in:
11来了 2024-12-19 23:06:39 +08:00
parent 15f55e4df4
commit 66e624c636

View File

@ -28,9 +28,9 @@ AQS 为构建锁和同步器提供了一些通用功能的实现。因此,使
### AQS 快速了解
在真正讲解 AQS 源码之前,需要对 AQS 有一个整体层面的认识。这里会先通过几个问题,从整体层面上认识 AQS了解 AQS 在整个 Java 并发中于的层面,之后在学习 AQS 源码的过程中,才能更加了解同步器和 AQS 之间的关系。
在真正讲解 AQS 源码之前,需要对 AQS 有一个整体层面的认识。这里会先通过几个问题,从整体层面上认识 AQS了解 AQS 在整个 Java 并发中所位于的层面,之后在学习 AQS 源码的过程中,才能更加了解同步器和 AQS 之间的关系。
- **`AQS` 的作用是什么?**
- **问题 1 `AQS` 的作用是什么?**
如果没有 AQS 的话,想要控制多线程的同步,需要通过 `synchronized` 关键字来完成,而 `synchronized` 又是 JVM 层面的,使用起来不太灵活。
@ -38,7 +38,7 @@ AQS 为构建锁和同步器提供了一些通用功能的实现。因此,使
为了控制多线程同步访问共享资源AQS 内部会提供一个队列,当线程获取不到共享资源时,会进入队列中等待,以此来实现多线程的同步访问。
- **`AQS` 为什么使用 CLH 锁队列的变体?**
- **问题 2 `AQS` 为什么使用 CLH 锁队列的变体?**
CLH 锁是基于自旋锁的优化。
@ -48,16 +48,17 @@ CLH 锁是基于自旋锁的优化。
而 AQS 又基于 CLH 锁进一步进行改进源码作者Doug Lea称 AQS 内部的队列为 CLH 锁队列的变体,主要进行了两点改进:
- 由 **自旋** 优化为 **自旋 + 阻塞** :自旋操作的性能很高,但大量的自旋操作比较占用 CPU 资源,因此在 CLH 锁队列的变体中会先通过自旋尝试获取锁,如果失败再进行阻塞等待。
- 由 **单向队列** 优化为 **双向队列** :在 CLH 锁队列的变体中,会对等待的线程进行阻塞操作,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒,因此增加了 `next` 指针,成为了双向队列。
1、由 **自旋** 优化为 **自旋 + 阻塞** :自旋操作的性能很高,但大量的自旋操作比较占用 CPU 资源,因此在 CLH 锁队列的变体中会先通过自旋尝试获取锁,如果失败再进行阻塞等待。
- **`AQS` 和同步器( `ReentrantLock``Semaphore` 等)之间的关系是怎样的?**
2、由 **单向队列** 优化为 **双向队列** :在 CLH 锁队列的变体中,会对等待的线程进行阻塞操作,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒,因此增加了 `next` 指针,成为了双向队列。
- **问题 3 `AQS` 和同步器( `ReentrantLock``Semaphore` 等)之间的关系是怎样的?**
AQS 是一个抽象类,为同步器提供了执行框架。 **获取资源的流程** 已经在 AQS 中定义好了,具体如何获取资源则由同步器来实现。
同步器只需要基于 AQS 重写获取和释放资源的模板方法即可,因此 AQS 是底座,同步器是上层应用。
- **`AQS` 的性能比较好,原因是什么?**
- **问题 4 `AQS` 的性能比较好,原因是什么?**
因为 AQS 里使用了 `CAS` + `线程阻塞/唤醒`
@ -65,7 +66,7 @@ AQS 是一个抽象类,为同步器提供了执行框架。 **获取资源的
但是如果一直通过 `CAS` 操作来更新数据,会比较占用 CPU。因此 AQS 同时结合了 `CAS``线程的阻塞/唤醒` 机制,当 `CAS` 没有成功获取资源时,会对线程进行阻塞,避免一直空转占用 CPU 资源。
- **`AQS` 中为什么 Node 节点需要不同的状态?**
- **问题 5 `AQS` 中为什么 Node 节点需要不同的状态?**
AQS 中的 `waitStatus` 状态类似于 **状态机** ,通过不同状态来表明 Node 节点的不同含义,并且根据不同操作,来控制状态之间的流转。
@ -592,17 +593,17 @@ private Node addWaiter(Node mode) {
由于 AQS 是底层同步工具,获取和释放资源的方法并没有提供具体实现,因此这里基于 `ReentrantLock` 来画图进行讲解。
假设总共有 3 个线程同时获取锁,线程分别为 `T1``T2``T3`
假设总共有 3 个线程尝试获取锁,线程分别为 `T1``T2``T3`
此时,假设线程 `T1` 先获取到锁,线程 `T2` 排队等待获取资源。在线程 `T2` 进入队列之前,需要对 AQS 内部队列进行初始化。初始的 `head` 节点状态为 `0` 。初始化后的队列如下图:
此时,假设线程 `T1` 先获取到锁,线程 `T2` 排队等待获取。在线程 `T2` 进入队列之前,需要对 AQS 内部队列进行初始化。`head` 节点在初始化后状态为 `0`AQS 内部初始化后的队列如下图:
![AQS acquire and release process 5.drawio](https://11laile-note-img.oss-cn-beijing.aliyuncs.com/AQS%20acquire%20and%20release%20process%205.drawio-173461521802737.png)
此时,线程 `T2` 尝试获取锁。由于线程 `T1` 持有,因此线程 `T2` 会进入队列中等待获取锁。同时会将前继节点( `head` 节点)的状态由 `0` 修改`SIGNAL` ,表示需要对 `head` 节点的后继节点进行唤醒。此时AQS 内部队列如下图所示:
此时,线程 `T2` 尝试获取锁。由于线程 `T1` 持有,因此线程 `T2` 会进入队列中等待获取锁。同时会将前继节点( `head` 节点)的状态由 `0` 更新`SIGNAL` ,表示需要对 `head` 节点的后继节点进行唤醒。此时AQS 内部队列如下图所示:
![AQS acquire and release process 4.drawio](https://11laile-note-img.oss-cn-beijing.aliyuncs.com/AQS%20acquire%20and%20release%20process%204.drawio-173461538992839.png)
此时,线程 `T2` 尝试获取锁。由于线程 `T1` 持有所,因此线程 `T2` 会进入队列中等待获取锁。同时会将前继节点(线程 `T2` 节点)的状态由 `0` 修改`SIGNAL` ,表示线程 `T2` 节点需要对后继节点进行唤醒。此时AQS 内部队列如下图所示:
此时,线程 `T3` 尝试获取锁。由于线程 `T1` 持有锁,因此线程 `T3` 会进入队列中等待获取锁。同时会将前继节点(线程 `T2` 节点)的状态由 `0` 更新`SIGNAL` ,表示线程 `T2` 节点需要对后继节点进行唤醒。此时AQS 内部队列如下图所示:
![AQS acquire and release process.drawio](https://11laile-note-img.oss-cn-beijing.aliyuncs.com/AQS%20acquire%20and%20release%20process.drawio.png)
@ -612,9 +613,7 @@ private Node addWaiter(Node mode) {
![AQS acquire and release process 2.drawio](https://11laile-note-img.oss-cn-beijing.aliyuncs.com/AQS%20acquire%20and%20release%20process%202.drawio-173461691867746.png)
此时,线程 `T2` 释放锁,会唤醒后继节点 `T3` 。线程 `T3` 获取到锁之后,同样也退出等待队列,即将线程 `T3` 节点变为 `head` 节点来退出资源获取的等待。
此时 AQS 内部队列如下所示:
此时,假设线程 `T2` 释放锁,会唤醒后继节点 `T3` 。线程 `T3` 获取到锁之后,同样也退出等待队列,即将线程 `T3` 节点变为 `head` 节点来退出资源获取的等待。此时 AQS 内部队列如下所示:
![AQS acquire and release process 3.drawio](https://11laile-note-img.oss-cn-beijing.aliyuncs.com/AQS%20acquire%20and%20release%20process%203.drawio-173461705733148.png)