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

Update reentrantlock.md

This commit is contained in:
guide 2021-12-06 19:52:34 +08:00
parent 5b97792e62
commit 2be829c35a

View File

@ -11,13 +11,13 @@ tag:
## 前言 ## 前言
Java中的大部分同步类Lock、Semaphore、ReentrantLock等都是基于AbstractQueuedSynchronizer简称为AQS实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。本文会从应用层逐渐深入到原理层并通过ReentrantLock的基本特性和ReentrantLock与AQS的关联来深入解读AQS相关独占锁的知识点同时采取问答的模式来帮助大家理解AQS。由于篇幅原因本篇文章主要阐述AQS中独占锁的逻辑和Sync Queue不讲述包含共享锁和Condition Queue的部分本篇文章核心为AQS原理剖析只是简单介绍了ReentrantLock感兴趣同学可以阅读一下ReentrantLock的源码 Java 中的大部分同步类Lock、Semaphore、ReentrantLock 等)都是基于 AbstractQueuedSynchronizer简称为 AQS实现的。AQS 是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。本文会从应用层逐渐深入到原理层,并通过 ReentrantLock 的基本特性和 ReentrantLock AQS 的关联,来深入解读 AQS 相关独占锁的知识点,同时采取问答的模式来帮助大家理解 AQS。由于篇幅原因本篇文章主要阐述 AQS 中独占锁的逻辑和 Sync Queue不讲述包含共享锁和 Condition Queue 的部分(本篇文章核心为 AQS 原理剖析,只是简单介绍了 ReentrantLock感兴趣同学可以阅读一下 ReentrantLock 的源码)。
## 1 ReentrantLock ## 1 ReentrantLock
### 1.1 ReentrantLock特性概览 ### 1.1 ReentrantLock 特性概览
ReentrantLock意思为可重入锁指的是一个线程能够对一个临界资源重复加锁。为了帮助大家更好地理解ReentrantLock的特性我们先将ReentrantLock跟常用的Synchronized进行比较其特性如下蓝色部分为本篇文章主要剖析的点 ReentrantLock 意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。为了帮助大家更好地理解 ReentrantLock 的特性,我们先将 ReentrantLock 跟常用的 Synchronized 进行比较,其特性如下(蓝色部分为本篇文章主要剖析的点):
![img](https://p0.meituan.net/travelcube/412d294ff5535bbcddc0d979b2a339e6102264.png) ![img](https://p0.meituan.net/travelcube/412d294ff5535bbcddc0d979b2a339e6102264.png)
@ -55,9 +55,9 @@ public void test () throw Exception {
} }
``` ```
### 1.2 ReentrantLock与AQS的关联 ### 1.2 ReentrantLock AQS 的关联
通过上文我们已经了解ReentrantLock支持公平锁和非公平锁关于公平锁和非公平锁的原理分析可参考《[不可不说的Java“锁”事](https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651749434&idx=3&sn=5ffa63ad47fe166f2f1a9f604ed10091&chksm=bd12a5778a652c61509d9e718ab086ff27ad8768586ea9b38c3dcf9e017a8e49bcae3df9bcc8&scene=38#wechat_redirect)》并且ReentrantLock的底层就是由AQS来实现的。那么ReentrantLock是如何通过公平锁和非公平锁与AQS关联起来呢 我们着重从这两者的加锁过程来理解一下它们与AQS之间的关系加锁过程中与AQS的关联比较明显解锁流程后续会介绍 通过上文我们已经了解ReentrantLock 支持公平锁和非公平锁(关于公平锁和非公平锁的原理分析,可参考《[不可不说的 Java“锁”事](https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651749434&idx=3&sn=5ffa63ad47fe166f2f1a9f604ed10091&chksm=bd12a5778a652c61509d9e718ab086ff27ad8768586ea9b38c3dcf9e017a8e49bcae3df9bcc8&scene=38#wechat_redirect)》),并且 ReentrantLock 的底层就是由 AQS 来实现的。那么 ReentrantLock 是如何通过公平锁和非公平锁与 AQS 关联起来呢? 我们着重从这两者的加锁过程来理解一下它们与 AQS 之间的关系(加锁过程中与 AQS 的关联比较明显,解锁流程后续会介绍)。
非公平锁源码中的加锁流程如下: 非公平锁源码中的加锁流程如下:
@ -79,18 +79,18 @@ static final class NonfairSync extends Sync {
这块代码的含义为: 这块代码的含义为:
- 若通过CAS设置变量State同步状态成功也就是获取锁成功则将当前线程设置为独占线程。 - 若通过 CAS 设置变量 State同步状态成功也就是获取锁成功则将当前线程设置为独占线程。
- 若通过CAS设置变量State同步状态失败也就是获取锁失败则进入Acquire方法进行后续处理。 - 若通过 CAS 设置变量 State同步状态失败也就是获取锁失败则进入 Acquire 方法进行后续处理。
第一步很好理解,但第二步获取锁失败后,后续的处理策略是怎么样的呢?这块可能会有以下思考: 第一步很好理解,但第二步获取锁失败后,后续的处理策略是怎么样的呢?这块可能会有以下思考:
- 某个线程获取锁失败的后续流程是什么呢?有以下两种可能: - 某个线程获取锁失败的后续流程是什么呢?有以下两种可能:
(1) 将当前线程获锁结果设置为失败获取锁流程结束。这种设计会极大降低系统的并发度并不满足我们实际的需求。所以就需要下面这种流程也就是AQS框架的处理流程。 (1) 将当前线程获锁结果设置为失败,获取锁流程结束。这种设计会极大降低系统的并发度,并不满足我们实际的需求。所以就需要下面这种流程,也就是 AQS 框架的处理流程。
(2) 存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。 (2) 存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。
- 对于问题1的第二种情况既然说到了排队等候机制那么就一定会有某种队列形成这样的队列是什么数据结构呢 - 对于问题 1 的第二种情况,既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
- 处于排队等候机制中的线程,什么时候可以有机会获取锁呢? - 处于排队等候机制中的线程,什么时候可以有机会获取锁呢?
- 如果处于排队等候机制中的线程一直无法获取锁,还是需要一直等待吗,还是有别的策略来解决这一问题? - 如果处于排队等候机制中的线程一直无法获取锁,还是需要一直等待吗,还是有别的策略来解决这一问题?
@ -100,7 +100,7 @@ static final class NonfairSync extends Sync {
// java.util.concurrent.locks.ReentrantLock#FairSync // java.util.concurrent.locks.ReentrantLock#FairSync
static final class FairSync extends Sync { static final class FairSync extends Sync {
... ...
final void lock() { final void lock() {
acquire(1); acquire(1);
} }
@ -108,41 +108,41 @@ static final class FairSync extends Sync {
} }
``` ```
看到这块代码我们可能会存在这种疑问Lock函数通过Acquire方法进行加锁但是具体是如何加锁的呢 看到这块代码我们可能会存在这种疑问Lock 函数通过 Acquire 方法进行加锁,但是具体是如何加锁的呢?
结合公平锁和非公平锁的加锁流程虽然流程上有一定的不同但是都调用了Acquire方法而Acquire方法是FairSync和UnfairSync的父类AQS中的核心方法。 结合公平锁和非公平锁的加锁流程,虽然流程上有一定的不同,但是都调用了 Acquire 方法,而 Acquire 方法是 FairSync UnfairSync 的父类 AQS 中的核心方法。
对于上边提到的问题其实在ReentrantLock类源码中都无法解答而这些问题的答案都是位于Acquire方法所在的类AbstractQueuedSynchronizer中也就是本文的核心——AQS。下面我们会对AQS以及ReentrantLock和AQS的关联做详细介绍相关问题答案会在2.3.5小节中解答)。 对于上边提到的问题,其实在 ReentrantLock 类源码中都无法解答,而这些问题的答案,都是位于 Acquire 方法所在的类 AbstractQueuedSynchronizer 也就是本文的核心——AQS。下面我们会对 AQS 以及 ReentrantLock AQS 的关联做详细介绍(相关问题答案会在 2.3.5 小节中解答)。
## 2 AQS ## 2 AQS
首先我们通过下面的架构图来整体了解一下AQS框架 首先,我们通过下面的架构图来整体了解一下 AQS 框架:
![](https://p1.meituan.net/travelcube/82077ccf14127a87b77cefd1ccf562d3253591.png) ![](https://p1.meituan.net/travelcube/82077ccf14127a87b77cefd1ccf562d3253591.png)
- 上图中有颜色的为Method无颜色的为Attribution。 - 上图中有颜色的为 Method无颜色的为 Attribution。
- 总的来说AQS框架共分为五层自上而下由浅入深从AQS对外暴露的API到底层基础数据。 - 总的来说AQS 框架共分为五层,自上而下由浅入深,从 AQS 对外暴露的 API 到底层基础数据。
- 当有自定义同步器接入时只需重写第一层所需要的部分方法即可不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时先经过第一层的API进入AQS内部方法然后经过第二层进行锁的获取接着对于获取锁失败的流程进入第三层和第四层的等待队列处理而这些处理方式均依赖于第五层的基础数据提供层。 - 当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,先经过第一层的 API 进入 AQS 内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层。
下面我们会从整体到细节从流程到方法逐一剖析AQS框架主要分析过程如下 下面我们会从整体到细节,从流程到方法逐一剖析 AQS 框架,主要分析过程如下:
![](https://p1.meituan.net/travelcube/d2f7f7fffdc30d85d17b44266c3ab05323338.png) ![](https://p1.meituan.net/travelcube/d2f7f7fffdc30d85d17b44266c3ab05323338.png)
### 2.1 原理概览 ### 2.1 原理概览
AQS核心思想是如果被请求的共享资源空闲那么就将当前请求资源的线程设置为有效的工作线程将共享资源设置为锁定状态如果共享资源被占用就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的将暂时获取不到锁的线程加入到队列中。 AQS 核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是 CLH 队列的变体实现的,将暂时获取不到锁的线程加入到队列中。
CLHCraig、Landin and Hagersten队列是单向链表AQS中的队列是CLH变体的虚拟双向队列FIFOAQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。 CLHCraig、Landin and Hagersten 队列是单向链表AQS 中的队列是 CLH 变体的虚拟双向队列FIFOAQS 是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
主要原理图如下: 主要原理图如下:
![](https://p0.meituan.net/travelcube/7132e4cef44c26f62835b197b239147b18062.png) ![](https://p0.meituan.net/travelcube/7132e4cef44c26f62835b197b239147b18062.png)
AQS使用一个Volatile的int类型的成员变量来表示同步状态通过内置的FIFO队列来完成资源获取的排队工作通过CAS完成对State值的修改。 AQS 使用一个 Volatile int 类型的成员变量来表示同步状态,通过内置的 FIFO 队列来完成资源获取的排队工作,通过 CAS 完成对 State 值的修改。
#### 2.1.1 AQS数据结构 #### 2.1.1 AQS 数据结构
先来看下AQS中最基本的数据结构——NodeNode即为上面CLH变体队列中的节点。 先来看下 AQS 中最基本的数据结构——NodeNode 即为上面 CLH 变体队列中的节点。
![](https://p1.meituan.net/travelcube/960271cf2b5c8a185eed23e98b72c75538637.png) ![](https://p1.meituan.net/travelcube/960271cf2b5c8a185eed23e98b72c75538637.png)
@ -153,8 +153,8 @@ AQS使用一个Volatile的int类型的成员变量来表示同步状态通过
| waitStatus | 当前节点在队列中的状态 | | waitStatus | 当前节点在队列中的状态 |
| thread | 表示处于该节点的线程 | | thread | 表示处于该节点的线程 |
| prev | 前驱指针 | | prev | 前驱指针 |
| predecessor | 返回前驱节点没有的话抛出npe | | predecessor | 返回前驱节点,没有的话抛出 npe |
| nextWaiter | 指向下一个处于CONDITION状态的节点由于本篇文章不讲述Condition Queue队列这个指针不多介绍 | | nextWaiter | 指向下一个处于 CONDITION 状态的节点(由于本篇文章不讲述 Condition Queue 队列,这个指针不多介绍) |
| next | 后继指针 | | next | 后继指针 |
线程两种锁的模式: 线程两种锁的模式:
@ -164,19 +164,19 @@ AQS使用一个Volatile的int类型的成员变量来表示同步状态通过
| SHARED | 表示线程以共享的模式等待锁 | | SHARED | 表示线程以共享的模式等待锁 |
| EXCLUSIVE | 表示线程正在以独占的方式等待锁 | | EXCLUSIVE | 表示线程正在以独占的方式等待锁 |
waitStatus有下面几个枚举值 waitStatus 有下面几个枚举值:
| 枚举 | 含义 | | 枚举 | 含义 |
| :-------- | :--------------------------------------------- | | :-------- | :----------------------------------------------- |
| 0 | 当一个Node被初始化的时候的默认值 | | 0 | 当一个 Node 被初始化的时候的默认值 |
| CANCELLED | 为1表示线程获取锁的请求已经取消了 | | CANCELLED | 为 1表示线程获取锁的请求已经取消了 |
| CONDITION | 为-2表示节点在等待队列中节点线程等待唤醒 | | CONDITION | 为-2表示节点在等待队列中节点线程等待唤醒 |
| PROPAGATE | 为-3当前线程处在SHARED情况下该字段才会使用 | | PROPAGATE | 为-3当前线程处在 SHARED 情况下,该字段才会使用 |
| SIGNAL | 为-1表示线程已经准备好了就等资源释放了 | | SIGNAL | 为-1表示线程已经准备好了就等资源释放了 |
#### 2.1.2 同步状态State #### 2.1.2 同步状态 State
在了解数据结构后接下来了解一下AQS的同步状态——State。AQS中维护了一个名为state的字段意为同步状态是由Volatile修饰的用于展示当前临界资源的获锁情况。 在了解数据结构后,接下来了解一下 AQS 的同步状态——State。AQS 中维护了一个名为 state 的字段,意为同步状态,是由 Volatile 修饰的,用于展示当前临界资源的获锁情况。
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -186,63 +186,63 @@ private volatile int state;
下面提供了几个访问这个字段的方法: 下面提供了几个访问这个字段的方法:
| 方法名 | 描述 | | 方法名 | 描述 |
| :----------------------------------------------------------- | :------------------- | | :----------------------------------------------------------- | :---------------------- |
| protected final int getState() | 获取State的值 | | protected final int getState() | 获取 State 的值 |
| protected final void setState(int newState) | 设置State的值 | | protected final void setState(int newState) | 设置 State 的值 |
| protected final boolean compareAndSetState(int expect, int update) | 使用CAS方式更新State | | protected final boolean compareAndSetState(int expect, int update) | 使用 CAS 方式更新 State |
这几个方法都是Final修饰的说明子类中无法重写它们。我们可以通过修改State字段表示的同步状态来实现多线程的独占模式和共享模式加锁过程 这几个方法都是 Final 修饰的,说明子类中无法重写它们。我们可以通过修改 State 字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程)。
![](https://p0.meituan.net/travelcube/27605d483e8935da683a93be015713f331378.png) ![](https://p0.meituan.net/travelcube/27605d483e8935da683a93be015713f331378.png)
![](https://p0.meituan.net/travelcube/3f1e1a44f5b7d77000ba4f9476189b2e32806.png) ![](https://p0.meituan.net/travelcube/3f1e1a44f5b7d77000ba4f9476189b2e32806.png)
对于我们自定义的同步工具需要自定义获取同步状态和释放状态的方式也就是AQS架构图中的第一层API层。 对于我们自定义的同步工具,需要自定义获取同步状态和释放状态的方式,也就是 AQS 架构图中的第一层API 层。
## 2.2 AQS重要方法与ReentrantLock的关联 ## 2.2 AQS 重要方法与 ReentrantLock 的关联
从架构图中可以得知AQS提供了大量用于自定义同步器实现的Protected方法。自定义同步器实现的相关方法也只是为了通过修改State字段来实现多线程的独占模式或者共享模式。自定义同步器需要实现以下方法ReentrantLock需要实现的方法如下并不是全部 从架构图中可以得知AQS 提供了大量用于自定义同步器实现的 Protected 方法。自定义同步器实现的相关方法也只是为了通过修改 State 字段来实现多线程的独占模式或者共享模式。自定义同步器需要实现以下方法ReentrantLock 需要实现的方法如下,并不是全部):
| 方法名 | 描述 | | 方法名 | 描述 |
| :------------------------------------------ | :----------------------------------------------------------- | | :------------------------------------------ | :----------------------------------------------------------- |
| protected boolean isHeldExclusively() | 该线程是否正在独占资源。只有用到Condition才需要去实现它。 | | protected boolean isHeldExclusively() | 该线程是否正在独占资源。只有用到 Condition 才需要去实现它。 |
| protected boolean tryAcquire(int arg) | 独占方式。arg为获取锁的次数尝试获取资源成功则返回True失败则返回False。 | | protected boolean tryAcquire(int arg) | 独占方式。arg 为获取锁的次数,尝试获取资源,成功则返回 True失败则返回 False。 |
| protected boolean tryRelease(int arg) | 独占方式。arg为释放锁的次数尝试释放资源成功则返回True失败则返回False。 | | protected boolean tryRelease(int arg) | 独占方式。arg 为释放锁的次数,尝试释放资源,成功则返回 True失败则返回 False。 |
| protected int tryAcquireShared(int arg) | 共享方式。arg为获取锁的次数尝试获取资源。负数表示失败0表示成功但没有剩余可用资源正数表示成功且有剩余资源。 | | protected int tryAcquireShared(int arg) | 共享方式。arg 为获取锁的次数尝试获取资源。负数表示失败0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 |
| protected boolean tryReleaseShared(int arg) | 共享方式。arg为释放锁的次数尝试释放资源如果释放后允许唤醒后续等待结点返回True否则返回False。 | | protected boolean tryReleaseShared(int arg) | 共享方式。arg 为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回 True否则返回 False。 |
一般来说自定义同步器要么是独占方式要么是共享方式它们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。AQS也支持自定义同步器同时实现独占和共享两种方式如ReentrantReadWriteLock。ReentrantLock是独占锁所以实现了tryAcquire-tryRelease。 一般来说,自定义同步器要么是独占方式,要么是共享方式,它们也只需实现 tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。AQS 也支持自定义同步器同时实现独占和共享两种方式,如 ReentrantReadWriteLock。ReentrantLock 是独占锁,所以实现了 tryAcquire-tryRelease。
以非公平锁为例这里主要阐述一下非公平锁与AQS之间方法的关联之处具体每一处核心方法的作用会在文章后面详细进行阐述。 以非公平锁为例,这里主要阐述一下非公平锁与 AQS 之间方法的关联之处,具体每一处核心方法的作用会在文章后面详细进行阐述。
![](https://p1.meituan.net/travelcube/b8b53a70984668bc68653efe9531573e78636.png) ![](https://p1.meituan.net/travelcube/b8b53a70984668bc68653efe9531573e78636.png)
为了帮助大家理解ReentrantLock和AQS之间方法的交互过程以非公平锁为例我们将加锁和解锁的交互流程单独拎出来强调一下以便于对后续内容的理解。 为了帮助大家理解 ReentrantLock AQS 之间方法的交互过程,以非公平锁为例,我们将加锁和解锁的交互流程单独拎出来强调一下,以便于对后续内容的理解。
![](https://p1.meituan.net/travelcube/7aadb272069d871bdee8bf3a218eed8136919.png) ![](https://p1.meituan.net/travelcube/7aadb272069d871bdee8bf3a218eed8136919.png)
加锁: 加锁:
- 通过ReentrantLock的加锁方法Lock进行加锁操作。 - 通过 ReentrantLock 的加锁方法 Lock 进行加锁操作。
- 会调用到内部类Sync的Lock方法由于Sync#lock是抽象方法根据ReentrantLock初始化选择的公平锁和非公平锁执行相关内部类的Lock方法本质上都会执行AQS的Acquire方法。 - 会调用到内部类 Sync Lock 方法,由于 Sync#lock 是抽象方法,根据 ReentrantLock 初始化选择的公平锁和非公平锁,执行相关内部类的 Lock 方法,本质上都会执行 AQS Acquire 方法。
- AQS的Acquire方法会执行tryAcquire方法但是由于tryAcquire需要自定义同步器实现因此执行了ReentrantLock中的tryAcquire方法由于ReentrantLock是通过公平锁和非公平锁内部类实现的tryAcquire方法因此会根据锁类型不同执行不同的tryAcquire。 - AQS Acquire 方法会执行 tryAcquire 方法,但是由于 tryAcquire 需要自定义同步器实现,因此执行了 ReentrantLock 中的 tryAcquire 方法,由于 ReentrantLock 是通过公平锁和非公平锁内部类实现的 tryAcquire 方法,因此会根据锁类型不同,执行不同的 tryAcquire。
- tryAcquire是获取锁逻辑获取失败后会执行框架AQS的后续逻辑跟ReentrantLock自定义同步器无关。 - tryAcquire 是获取锁逻辑,获取失败后,会执行框架 AQS 的后续逻辑,跟 ReentrantLock 自定义同步器无关。
解锁: 解锁:
- 通过ReentrantLock的解锁方法Unlock进行解锁。 - 通过 ReentrantLock 的解锁方法 Unlock 进行解锁。
- Unlock会调用内部类Sync的Release方法该方法继承于AQS。 - Unlock 会调用内部类 Sync Release 方法,该方法继承于 AQS。
- Release中会调用tryRelease方法tryRelease需要自定义同步器实现tryRelease只在ReentrantLock中的Sync实现因此可以看出释放锁的过程并不区分是否为公平锁。 - Release 中会调用 tryRelease 方法tryRelease 需要自定义同步器实现tryRelease 只在 ReentrantLock 中的 Sync 实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。
- 释放成功后所有处理由AQS框架完成与自定义同步器无关。 - 释放成功后,所有处理由 AQS 框架完成,与自定义同步器无关。
通过上面的描述大概可以总结出ReentrantLock加锁解锁时API层核心方法的映射关系。 通过上面的描述,大概可以总结出 ReentrantLock 加锁解锁时 API 层核心方法的映射关系。
![](https://p0.meituan.net/travelcube/f30c631c8ebbf820d3e8fcb6eee3c0ef18748.png) ![](https://p0.meituan.net/travelcube/f30c631c8ebbf820d3e8fcb6eee3c0ef18748.png)
## 2.3 通过ReentrantLock理解AQS ## 2.3 通过 ReentrantLock 理解 AQS
ReentrantLock中公平锁和非公平锁在底层是相同的这里以非公平锁为例进行分析。 ReentrantLock 中公平锁和非公平锁在底层是相同的,这里以非公平锁为例进行分析。
在非公平锁中,有一段这样的代码: 在非公平锁中,有一段这样的代码:
@ -261,7 +261,7 @@ static final class NonfairSync extends Sync {
} }
``` ```
看一下这个Acquire是怎么写的 看一下这个 Acquire 是怎么写的:
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -272,7 +272,7 @@ public final void acquire(int arg) {
} }
``` ```
再看一下tryAcquire方法 再看一下 tryAcquire 方法:
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -282,17 +282,17 @@ protected boolean tryAcquire(int arg) {
} }
``` ```
可以看出这里只是AQS的简单实现具体获取锁的实现方法是由各自的公平锁和非公平锁单独实现的以ReentrantLock为例。如果该方法返回了True则说明当前线程获取锁成功就不用往后执行了如果获取失败就需要加入到等待队列中。下面会详细解释线程是何时以及怎样被加入进等待队列中的。 可以看出,这里只是 AQS 的简单实现,具体获取锁的实现方法是由各自的公平锁和非公平锁单独实现的(以 ReentrantLock 为例)。如果该方法返回了 True则说明当前线程获取锁成功就不用往后执行了如果获取失败就需要加入到等待队列中。下面会详细解释线程是何时以及怎样被加入进等待队列中的。
### 2.3.1 线程加入等待队列 ### 2.3.1 线程加入等待队列
#### 2.3.1.1 加入队列的时机 #### 2.3.1.1 加入队列的时机
当执行Acquire(1)时会通过tryAcquire获取锁。在这种情况下如果获取锁失败就会调用addWaiter加入到等待队列中去。 当执行 Acquire(1)时,会通过 tryAcquire 获取锁。在这种情况下,如果获取锁失败,就会调用 addWaiter 加入到等待队列中去。
#### 2.3.1.2 如何加入队列 #### 2.3.1.2 如何加入队列
获取锁失败后会执行addWaiter(Node.EXCLUSIVE)加入等待队列,具体实现方法如下: 获取锁失败后,会执行 addWaiter(Node.EXCLUSIVE)加入等待队列,具体实现方法如下:
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -319,9 +319,9 @@ private final boolean compareAndSetTail(Node expect, Node update) {
主要的流程如下: 主要的流程如下:
- 通过当前的线程和锁模式新建一个节点。 - 通过当前的线程和锁模式新建一个节点。
- Pred指针指向尾节点Tail。 - Pred 指针指向尾节点 Tail。
- 将New中Node的Prev指针指向Pred。 - 将 New Node Prev 指针指向 Pred。
- 通过compareAndSetTail方法完成尾节点的设置。这个方法主要是对tailOffset和Expect进行比较如果tailOffset的Node和Expect的Node地址是相同的那么设置Tail的值为Update的值。 - 通过 compareAndSetTail 方法,完成尾节点的设置。这个方法主要是对 tailOffset Expect 进行比较,如果 tailOffset Node Expect Node 地址是相同的,那么设置 Tail 的值为 Update 的值。
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -333,15 +333,15 @@ static {
tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail")); tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus")); waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus"));
nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next")); nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next"));
} catch (Exception ex) { } catch (Exception ex) {
throw new Error(ex); throw new Error(ex);
} }
} }
``` ```
从AQS的静态代码块可以看出都是获取一个对象的属性相对于该对象在内存当中的偏移量这样我们就可以根据这个偏移量在对象内存当中找到这个属性。tailOffset指的是tail对应的偏移量所以这个时候会将new出来的Node置为当前队列的尾节点。同时由于是双向链表也需要将前一个节点指向尾节点。 AQS 的静态代码块可以看出都是获取一个对象的属性相对于该对象在内存当中的偏移量这样我们就可以根据这个偏移量在对象内存当中找到这个属性。tailOffset 指的是 tail 对应的偏移量,所以这个时候会将 new 出来的 Node 置为当前队列的尾节点。同时,由于是双向链表,也需要将前一个节点指向尾节点。
- 如果Pred指针是Null说明等待队列中没有元素或者当前Pred指针和Tail指向的位置不同说明被别的线程已经修改就需要看一下Enq的方法。 - 如果 Pred 指针是 Null说明等待队列中没有元素或者当前 Pred 指针和 Tail 指向的位置不同(说明被别的线程已经修改),就需要看一下 Enq 的方法。
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -363,18 +363,18 @@ private Node enq(final Node node) {
} }
``` ```
如果没有被初始化需要进行初始化一个头结点出来。但请注意初始化的头结点并不是当前线程节点而是调用了无参构造函数的节点。如果经历了初始化或者并发导致队列中有元素则与之前的方法相同。其实addWaiter就是一个在双端链表添加尾节点的操作需要注意的是双端链表的头结点是一个无参构造函数的头结点。 如果没有被初始化需要进行初始化一个头结点出来。但请注意初始化的头结点并不是当前线程节点而是调用了无参构造函数的节点。如果经历了初始化或者并发导致队列中有元素则与之前的方法相同。其实addWaiter 就是一个在双端链表添加尾节点的操作,需要注意的是,双端链表的头结点是一个无参构造函数的头结点。
总结一下,线程获取锁的时候,过程大体如下: 总结一下,线程获取锁的时候,过程大体如下:
1. 当没有线程获取到锁时线程1获取锁成功。 1. 当没有线程获取到锁时,线程 1 获取锁成功。
2. 线程2申请锁但是锁被线程1占有。 2. 线程 2 申请锁,但是锁被线程 1 占有。
![img](https://p0.meituan.net/travelcube/e9e385c3c68f62c67c8d62ab0adb613921117.png) ![img](https://p0.meituan.net/travelcube/e9e385c3c68f62c67c8d62ab0adb613921117.png)
1. 如果再有线程要获取锁,依次在队列中往后排队即可。 1. 如果再有线程要获取锁,依次在队列中往后排队即可。
回到上边的代码hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法。如果返回False说明当前线程可以争取共享资源如果返回True说明队列中存在有效节点当前线程必须加入到等待队列中。 回到上边的代码hasQueuedPredecessors 是公平锁加锁时判断等待队列中是否存在有效节点的方法。如果返回 False说明当前线程可以争取共享资源如果返回 True说明队列中存在有效节点当前线程必须加入到等待队列中。
```java ```java
// java.util.concurrent.locks.ReentrantLock // java.util.concurrent.locks.ReentrantLock
@ -390,9 +390,9 @@ public final boolean hasQueuedPredecessors() {
} }
``` ```
看到这里我们理解一下h != t && ((s = h.next) == null || s.thread != Thread.currentThread());为什么要判断的头结点的下一个节点?第一个节点储存的数据是什么? 看到这里,我们理解一下 h != t && ((s = h.next) == null || s.thread != Thread.currentThread());为什么要判断的头结点的下一个节点?第一个节点储存的数据是什么?
> 双向链表中第一个节点为虚节点其实并不存储任何信息只是占位。真正的第一个有数据的节点是在第二个节点开始的。当h != t时 如果(s = h.next) == null等待队列正在有线程进行初始化但只是进行到了Tail指向Head没有将Head指向Tail此时队列中有元素需要返回True这块具体见下边代码分析。 如果(s = h.next) != null说明此时队列中至少有一个有效节点。如果此时s.thread == Thread.currentThread()说明等待队列的第一个有效节点中的线程与当前线程相同那么当前线程是可以获取资源的如果s.thread != Thread.currentThread(),说明等待队列的第一个有效节点线程与当前线程不同,当前线程必须加入进等待队列。 > 双向链表中,第一个节点为虚节点,其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是在第二个节点开始的。当 h != t 时: 如果(s = h.next) == null等待队列正在有线程进行初始化但只是进行到了 Tail 指向 Head没有将 Head 指向 Tail此时队列中有元素需要返回 True这块具体见下边代码分析。 如果(s = h.next) != null说明此时队列中至少有一个有效节点。如果此时 s.thread == Thread.currentThread(),说明等待队列的第一个有效节点中的线程与当前线程相同,那么当前线程是可以获取资源的;如果 s.thread != Thread.currentThread(),说明等待队列的第一个有效节点线程与当前线程不同,当前线程必须加入进等待队列。
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer#enq // java.util.concurrent.locks.AbstractQueuedSynchronizer#enq
@ -409,7 +409,7 @@ if (t == null) { // Must initialize
} }
``` ```
节点入队不是原子操作所以会出现短暂的head != tail此时Tail指向最后一个节点而且Tail指向Head。如果Head没有指向Tail可见5、6、7行这种情况下也需要将相关线程加入队列中。所以这块代码是为了解决极端情况下的并发问题。 节点入队不是原子操作,所以会出现短暂的 head != tail此时 Tail 指向最后一个节点,而且 Tail 指向 Head。如果 Head 没有指向 Tail可见 5、6、7 行),这种情况下也需要将相关线程加入队列中。所以这块代码是为了解决极端情况下的并发问题。
#### 2.3.1.3 等待队列中线程出队列时机 #### 2.3.1.3 等待队列中线程出队列时机
@ -424,11 +424,11 @@ public final void acquire(int arg) {
} }
``` ```
上文解释了addWaiter方法这个方法其实就是把对应的线程以Node的数据结构形式加入到双端队列里返回的是一个包含该线程的Node。而这个Node会作为参数进入到acquireQueued方法中。acquireQueued方法可以对排队中的线程进行“获锁”操作。 上文解释了 addWaiter 方法,这个方法其实就是把对应的线程以 Node 的数据结构形式加入到双端队列里,返回的是一个包含该线程的 Node。而这个 Node 会作为参数,进入到 acquireQueued 方法中。acquireQueued 方法可以对排队中的线程进行“获锁”操作。
总的来说一个线程获取锁失败了被放入等待队列acquireQueued会把放入队列中的线程不断去获取锁直到获取成功或者不再需要获取中断 总的来说一个线程获取锁失败了被放入等待队列acquireQueued 会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)。
下面我们从“何时出队列”和“如何出队列”两个方向来分析一下acquireQueued源码 下面我们从“何时出队列?”和“如何出队列?”两个方向来分析一下 acquireQueued 源码:
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -462,7 +462,7 @@ final boolean acquireQueued(final Node node, int arg) {
} }
``` ```
setHead方法是把当前节点置为虚节点但并没有修改waitStatus因为它是一直需要用的数据。 setHead 方法是把当前节点置为虚节点,但并没有修改 waitStatus因为它是一直需要用的数据。
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -481,7 +481,7 @@ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; int ws = pred.waitStatus;
// 说明头结点处于唤醒状态 // 说明头结点处于唤醒状态
if (ws == Node.SIGNAL) if (ws == Node.SIGNAL)
return true; return true;
// 通过枚举值我们知道waitStatus>0是取消状态 // 通过枚举值我们知道waitStatus>0是取消状态
if (ws > 0) { if (ws > 0) {
do { do {
@ -497,7 +497,7 @@ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
} }
``` ```
parkAndCheckInterrupt主要用于挂起当前线程阻塞调用栈返回当前线程的中断状态。 parkAndCheckInterrupt 主要用于挂起当前线程,阻塞调用栈,返回当前线程的中断状态。
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -512,18 +512,18 @@ private final boolean parkAndCheckInterrupt() {
![](https://p0.meituan.net/travelcube/c124b76dcbefb9bdc778458064703d1135485.png) ![](https://p0.meituan.net/travelcube/c124b76dcbefb9bdc778458064703d1135485.png)
从上图可以看出跳出当前循环的条件是当“前置节点是头结点且当前线程获取锁成功”。为了防止因死循环导致CPU资源被浪费我们会判断前置节点的状态来决定是否要将当前线程挂起具体挂起流程用流程图表示如下shouldParkAfterFailedAcquire流程 从上图可以看出,跳出当前循环的条件是当“前置节点是头结点,且当前线程获取锁成功”。为了防止因死循环导致 CPU 资源被浪费我们会判断前置节点的状态来决定是否要将当前线程挂起具体挂起流程用流程图表示如下shouldParkAfterFailedAcquire 流程):
![](https://p0.meituan.net/travelcube/9af16e2481ad85f38ca322a225ae737535740.png) ![](https://p0.meituan.net/travelcube/9af16e2481ad85f38ca322a225ae737535740.png)
从队列中释放节点的疑虑打消了,那么又有新问题了: 从队列中释放节点的疑虑打消了,那么又有新问题了:
- shouldParkAfterFailedAcquire中取消节点是怎么生成的呢什么时候会把一个节点的waitStatus设置为-1 - shouldParkAfterFailedAcquire 中取消节点是怎么生成的呢?什么时候会把一个节点的 waitStatus 设置为-1
- 是在什么时间释放节点通知到被挂起的线程呢? - 是在什么时间释放节点通知到被挂起的线程呢?
### 2.3.2 CANCELLED状态节点生成 ### 2.3.2 CANCELLED 状态节点生成
acquireQueued方法中的Finally代码 acquireQueued 方法中的 Finally 代码:
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -547,7 +547,7 @@ final boolean acquireQueued(final Node node, int arg) {
} }
``` ```
通过cancelAcquire方法将Node的状态标记为CANCELLED。接下来我们逐行来分析这个方法的原理 通过 cancelAcquire 方法,将 Node 的状态标记为 CANCELLED。接下来我们逐行来分析这个方法的原理
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -590,14 +590,14 @@ private void cancelAcquire(Node node) {
当前的流程: 当前的流程:
- 获取当前节点的前驱节点如果前驱节点的状态是CANCELLED那就一直往前遍历找到第一个waitStatus <= 0的节点将找到的Pred节点和当前Node关联将当前Node设置为CANCELLED。 - 获取当前节点的前驱节点,如果前驱节点的状态是 CANCELLED那就一直往前遍历找到第一个 waitStatus <= 0 的节点,将找到的 Pred 节点和当前 Node 关联,将当前 Node 设置为 CANCELLED。
- 根据当前节点的位置,考虑以下三种情况: - 根据当前节点的位置,考虑以下三种情况:
(1) 当前节点是尾节点。 (1) 当前节点是尾节点。
(2) 当前节点是Head的后继节点。 (2) 当前节点是 Head 的后继节点。
(3) 当前节点不是Head的后继节点也不是尾节点。 (3) 当前节点不是 Head 的后继节点,也不是尾节点。
根据上述第二条,我们来分析每一种情况的流程。 根据上述第二条,我们来分析每一种情况的流程。
@ -605,17 +605,17 @@ private void cancelAcquire(Node node) {
![](https://p1.meituan.net/travelcube/b845211ced57561c24f79d56194949e822049.png) ![](https://p1.meituan.net/travelcube/b845211ced57561c24f79d56194949e822049.png)
当前节点是Head的后继节点。 当前节点是 Head 的后继节点。
![](https://p1.meituan.net/travelcube/ab89bfec875846e5028a4f8fead32b7117975.png) ![](https://p1.meituan.net/travelcube/ab89bfec875846e5028a4f8fead32b7117975.png)
当前节点不是Head的后继节点也不是尾节点。 当前节点不是 Head 的后继节点,也不是尾节点。
![](https://p0.meituan.net/travelcube/45d0d9e4a6897eddadc4397cf53d6cd522452.png) ![](https://p0.meituan.net/travelcube/45d0d9e4a6897eddadc4397cf53d6cd522452.png)
通过上面的流程我们对于CANCELLED节点状态的产生和变化已经有了大致的了解但是为什么所有的变化都是对Next指针进行了操作而没有对Prev指针进行操作呢什么情况下会对Prev指针进行操作 通过上面的流程,我们对于 CANCELLED 节点状态的产生和变化已经有了大致的了解,但是为什么所有的变化都是对 Next 指针进行了操作,而没有对 Prev 指针进行操作呢?什么情况下会对 Prev 指针进行操作?
> 执行cancelAcquire的时候当前节点的前置节点可能已经从队列中出去了已经执行过Try代码块中的shouldParkAfterFailedAcquire方法了如果此时修改Prev指针有可能会导致Prev指向另一个已经移除队列的Node因此这块变化Prev指针不安全。 shouldParkAfterFailedAcquire方法中会执行下面的代码其实就是在处理Prev指针。shouldParkAfterFailedAcquire是获取锁失败的情况下才会执行进入该方法后说明共享资源已被获取当前节点之前的节点都不会出现变化因此这个时候变更Prev指针比较安全。 > 执行 cancelAcquire 的时候,当前节点的前置节点可能已经从队列中出去了(已经执行过 Try 代码块中的 shouldParkAfterFailedAcquire 方法了),如果此时修改 Prev 指针,有可能会导致 Prev 指向另一个已经移除队列的 Node因此这块变化 Prev 指针不安全。 shouldParkAfterFailedAcquire 方法中,会执行下面的代码,其实就是在处理 Prev 指针。shouldParkAfterFailedAcquire 是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化,因此这个时候变更 Prev 指针比较安全。
> >
> ```java > ```java
> do { > do {
@ -625,7 +625,7 @@ private void cancelAcquire(Node node) {
### 2.3.3 如何解锁 ### 2.3.3 如何解锁
我们已经剖析了加锁过程中的基本流程接下来再对解锁的基本流程进行分析。由于ReentrantLock在解锁的时候并不区分公平锁和非公平锁所以我们直接看解锁的源码 我们已经剖析了加锁过程中的基本流程,接下来再对解锁的基本流程进行分析。由于 ReentrantLock 在解锁的时候,并不区分公平锁和非公平锁,所以我们直接看解锁的源码:
```java ```java
// java.util.concurrent.locks.ReentrantLock // java.util.concurrent.locks.ReentrantLock
@ -651,7 +651,7 @@ public final boolean release(int arg) {
} }
``` ```
在ReentrantLock里面的公平锁和非公平锁的父类Sync定义了可重入锁的释放锁机制。 ReentrantLock 里面的公平锁和非公平锁的父类 Sync 定义了可重入锁的释放锁机制。
```java ```java
// java.util.concurrent.locks.ReentrantLock.Sync // java.util.concurrent.locks.ReentrantLock.Sync
@ -693,15 +693,15 @@ public final boolean release(int arg) {
} }
``` ```
这里的判断条件为什么是h != null && h.waitStatus != 0 这里的判断条件为什么是 h != null && h.waitStatus != 0
> h == null Head还没初始化。初始情况下head == null第一个节点入队Head会被初始化一个虚拟节点。所以说这里如果还没来得及入队就会出现head == null 的情况。 > h == null Head 还没初始化。初始情况下head == null第一个节点入队Head 会被初始化一个虚拟节点。所以说,这里如果还没来得及入队,就会出现 head == null 的情况。
> >
> h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒。 > h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒。
> >
> h != null && waitStatus < 0 表明后继节点可能被阻塞了需要唤醒 > h != null && waitStatus < 0 表明后继节点可能被阻塞了需要唤醒
再看一下unparkSuccessor方法 再看一下 unparkSuccessor 方法:
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -727,9 +727,9 @@ private void unparkSuccessor(Node node) {
} }
``` ```
为什么要从后往前找第一个非Cancelled的节点呢原因如下。 为什么要从后往前找第一个非 Cancelled 的节点呢?原因如下。
之前的addWaiter方法 之前的 addWaiter 方法:
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -750,13 +750,13 @@ private Node addWaiter(Node mode) {
} }
``` ```
我们从这里可以看到节点入队并不是原子操作也就是说node.prev = pred; compareAndSetTail(pred, node) 这两个地方可以看作Tail入队的原子操作但是此时pred.next = node;还没执行如果这个时候执行了unparkSuccessor方法就没办法从前往后找了所以需要从后往前找。还有一点原因在产生CANCELLED状态节点的时候先断开的是Next指针Prev指针并未断开因此也是必须要从后往前遍历才能够遍历完全部的Node。 我们从这里可以看到节点入队并不是原子操作也就是说node.prev = pred; compareAndSetTail(pred, node) 这两个地方可以看作 Tail 入队的原子操作,但是此时 pred.next = node;还没执行,如果这个时候执行了 unparkSuccessor 方法,就没办法从前往后找了,所以需要从后往前找。还有一点原因,在产生 CANCELLED 状态节点的时候,先断开的是 Next 指针Prev 指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的 Node。
综上所述如果是从前往后找由于极端情况下入队的非原子操作和CANCELLED节点产生过程中断开Next指针的操作可能会导致无法遍历所有的节点。所以唤醒对应的线程后对应的线程就会继续往下执行。继续执行acquireQueued方法以后中断如何处理 综上所述,如果是从前往后找,由于极端情况下入队的非原子操作和 CANCELLED 节点产生过程中断开 Next 指针的操作,可能会导致无法遍历所有的节点。所以,唤醒对应的线程后,对应的线程就会继续往下执行。继续执行 acquireQueued 方法以后,中断如何处理?
### 2.3.4 中断恢复后的执行流程 ### 2.3.4 中断恢复后的执行流程
唤醒后会执行return Thread.interrupted();,这个函数返回的是当前执行线程的中断状态,并清除。 唤醒后,会执行 return Thread.interrupted();,这个函数返回的是当前执行线程的中断状态,并清除。
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -767,7 +767,7 @@ private final boolean parkAndCheckInterrupt() {
} }
``` ```
再回到acquireQueued代码当parkAndCheckInterrupt返回True或者False的时候interrupted的值不同但都会执行下次循环。如果这个时候获取锁成功就会把当前interrupted返回。 再回到 acquireQueued 代码,当 parkAndCheckInterrupt 返回 True 或者 False 的时候interrupted 的值不同,但都会执行下次循环。如果这个时候获取锁成功,就会把当前 interrupted 返回。
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -794,7 +794,7 @@ final boolean acquireQueued(final Node node, int arg) {
} }
``` ```
如果acquireQueued为True就会执行selfInterrupt方法。 如果 acquireQueued True就会执行 selfInterrupt 方法。
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -804,16 +804,16 @@ static void selfInterrupt() {
} }
``` ```
该方法其实是为了中断线程。但为什么获取了锁以后还要中断线程呢这部分属于Java提供的协作式中断知识内容感兴趣同学可以查阅一下。这里简单介绍一下 该方法其实是为了中断线程。但为什么获取了锁以后还要中断线程呢?这部分属于 Java 提供的协作式中断知识内容,感兴趣同学可以查阅一下。这里简单介绍一下:
1. 当中断线程被唤醒时并不知道被唤醒的原因可能是当前线程在等待中被中断也可能是释放了锁以后被唤醒。因此我们通过Thread.interrupted()方法检查中断标记该方法返回了当前线程的中断状态并将当前线程的中断标识设置为False并记录下来如果发现该线程被中断过就再中断一次。 1. 当中断线程被唤醒时,并不知道被唤醒的原因,可能是当前线程在等待中被中断,也可能是释放了锁以后被唤醒。因此我们通过 Thread.interrupted()方法检查中断标记(该方法返回了当前线程的中断状态,并将当前线程的中断标识设置为 False并记录下来如果发现该线程被中断过就再中断一次。
2. 线程在等待资源的过程中被唤醒,唤醒后还是会不断地去尝试获取锁,直到抢到锁为止。也就是说,在整个流程中,并不响应中断,只是记录中断记录。最后抢到锁返回了,那么如果被中断过的话,就需要补充一次中断。 2. 线程在等待资源的过程中被唤醒,唤醒后还是会不断地去尝试获取锁,直到抢到锁为止。也就是说,在整个流程中,并不响应中断,只是记录中断记录。最后抢到锁返回了,那么如果被中断过的话,就需要补充一次中断。
这里的处理方式主要是运用线程池中基本运作单元Worder中的runWorker通过Thread.interrupted()进行额外的判断处理感兴趣的同学可以看下ThreadPoolExecutor源码。 这里的处理方式主要是运用线程池中基本运作单元 Worder 中的 runWorker通过 Thread.interrupted()进行额外的判断处理,感兴趣的同学可以看下 ThreadPoolExecutor 源码。
### 2.3.5 小结 ### 2.3.5 小结
我们在1.3小节中提出了一些问题,现在来回答一下。 我们在 1.3 小节中提出了一些问题,现在来回答一下。
> Q某个线程获取锁失败的后续流程是什么呢 > Q某个线程获取锁失败的后续流程是什么呢
> >
@ -821,25 +821,25 @@ static void selfInterrupt() {
> >
> Q既然说到了排队等候机制那么就一定会有某种队列形成这样的队列是什么数据结构呢 > Q既然说到了排队等候机制那么就一定会有某种队列形成这样的队列是什么数据结构呢
> >
> A是CLH变体的FIFO双端队列。 > A CLH 变体的 FIFO 双端队列。
> >
> Q处于排队等候机制中的线程什么时候可以有机会获取锁呢 > Q处于排队等候机制中的线程什么时候可以有机会获取锁呢
> >
> A可以详细看下2.3.1.3小节。 > A可以详细看下 2.3.1.3 小节。
> >
> Q如果处于排队等候机制中的线程一直无法获取锁需要一直等待么还是有别的策略来解决这一问题 > Q如果处于排队等候机制中的线程一直无法获取锁需要一直等待么还是有别的策略来解决这一问题
> >
> A线程所在节点的状态会变成取消状态取消状态的节点会从队列中释放具体可见2.3.2小节。 > A线程所在节点的状态会变成取消状态取消状态的节点会从队列中释放具体可见 2.3.2 小节。
> >
> QLock函数通过Acquire方法进行加锁但是具体是如何加锁的呢 > QLock 函数通过 Acquire 方法进行加锁,但是具体是如何加锁的呢?
> >
> AAQS的Acquire会调用tryAcquire方法tryAcquire由各个自定义同步器实现通过tryAcquire完成加锁过程。 > AAQS Acquire 会调用 tryAcquire 方法tryAcquire 由各个自定义同步器实现,通过 tryAcquire 完成加锁过程。
## 3 AQS应用 ## 3 AQS 应用
### 3.1 ReentrantLock的可重入应用 ### 3.1 ReentrantLock 的可重入应用
ReentrantLock的可重入性是AQS很好的应用之一在了解完上述知识点以后我们很容易得知ReentrantLock实现可重入的方法。在ReentrantLock里面不管是公平锁还是非公平锁都有一段逻辑。 ReentrantLock 的可重入性是 AQS 很好的应用之一,在了解完上述知识点以后,我们很容易得知 ReentrantLock 实现可重入的方法。在 ReentrantLock 里面,不管是公平锁还是非公平锁,都有一段逻辑。
公平锁: 公平锁:
@ -881,7 +881,7 @@ else if (current == getExclusiveOwnerThread()) {
} }
``` ```
从上面这两段都可以看到有一个同步状态State来控制整体可重入的情况。State是Volatile修饰的用于保证一定的可见性和有序性。 从上面这两段都可以看到,有一个同步状态 State 来控制整体可重入的情况。State Volatile 修饰的,用于保证一定的可见性和有序性。
```java ```java
// java.util.concurrent.locks.AbstractQueuedSynchronizer // java.util.concurrent.locks.AbstractQueuedSynchronizer
@ -889,27 +889,27 @@ else if (current == getExclusiveOwnerThread()) {
private volatile int state; private volatile int state;
``` ```
接下来看State这个字段主要的过程 接下来看 State 这个字段主要的过程:
1. State初始化的时候为0表示没有任何线程持有锁。 1. State 初始化的时候为 0表示没有任何线程持有锁。
2. 当有线程持有该锁时,值就会在原来的基础上+1同一个线程多次获得锁是就会多次+1这里就是可重入的概念。 2. 当有线程持有该锁时,值就会在原来的基础上+1同一个线程多次获得锁是就会多次+1这里就是可重入的概念。
3. 解锁也是对这个字段-1一直到0此线程对锁释放。 3. 解锁也是对这个字段-1一直到 0此线程对锁释放。
### 3.2 JUC中的应用场景 ### 3.2 JUC 中的应用场景
除了上边ReentrantLock的可重入性的应用AQS作为并发编程的框架为很多其他同步工具提供了良好的解决方案。下面列出了JUC中的几种同步工具大体介绍一下AQS的应用场景 除了上边 ReentrantLock 的可重入性的应用AQS 作为并发编程的框架,为很多其他同步工具提供了良好的解决方案。下面列出了 JUC 中的几种同步工具,大体介绍一下 AQS 的应用场景:
| 同步工具 | 同步工具与AQS的关联 | | 同步工具 | 同步工具与 AQS 的关联 |
| :--------------------- | :----------------------------------------------------------- | | :--------------------- | :----------------------------------------------------------- |
| ReentrantLock | 使用AQS保存锁重复持有的次数。当一个线程获取锁时ReentrantLock记录当前获得锁的线程标识用于检测是否重复获取以及错误线程试图解锁操作时异常情况的处理。 | | ReentrantLock | 使用 AQS 保存锁重复持有的次数。当一个线程获取锁时ReentrantLock 记录当前获得锁的线程标识,用于检测是否重复获取,以及错误线程试图解锁操作时异常情况的处理。 |
| Semaphore | 使用AQS同步状态来保存信号量的当前计数。tryRelease会增加计数acquireShared会减少计数。 | | Semaphore | 使用 AQS 同步状态来保存信号量的当前计数。tryRelease 会增加计数acquireShared 会减少计数。 |
| CountDownLatch | 使用AQS同步状态来表示计数。计数为0时所有的Acquire操作CountDownLatch的await方法才可以通过。 | | CountDownLatch | 使用 AQS 同步状态来表示计数。计数为 0 时,所有的 Acquire 操作CountDownLatch await 方法)才可以通过。 |
| ReentrantReadWriteLock | 使用AQS同步状态中的16位保存写锁持有的次数剩下的16位用于保存读锁的持有次数。 | | ReentrantReadWriteLock | 使用 AQS 同步状态中的 16 位保存写锁持有的次数,剩下的 16 位用于保存读锁的持有次数。 |
| ThreadPoolExecutor | Worker利用AQS同步状态实现对独占线程变量的设置tryAcquire和tryRelease。 | | ThreadPoolExecutor | Worker 利用 AQS 同步状态实现对独占线程变量的设置tryAcquire tryRelease。 |
### 3.3 自定义同步工具 ### 3.3 自定义同步工具
了解AQS基本原理以后按照上面所说的AQS知识点自己实现一个同步工具。 了解 AQS 基本原理以后,按照上面所说的 AQS 知识点,自己实现一个同步工具。
```java ```java
public class LeeLock { public class LeeLock {
@ -931,20 +931,20 @@ public class LeeLock {
return getState() == 1; return getState() == 1;
} }
} }
private Sync sync = new Sync(); private Sync sync = new Sync();
public void lock () { public void lock () {
sync.acquire(1); sync.acquire(1);
} }
public void unlock () { public void unlock () {
sync.release(1); sync.release(1);
} }
} }
``` ```
通过我们自己定义的Lock完成一定的同步功能。 通过我们自己定义的 Lock 完成一定的同步功能。
```java ```java
public class LeeMain { public class LeeMain {
@ -981,14 +981,14 @@ public class LeeMain {
} }
``` ```
上述代码每次运行结果都会是20000。通过简单的几行代码就能实现同步功能这就是AQS的强大之处。 上述代码每次运行结果都会是 20000。通过简单的几行代码就能实现同步功能这就是 AQS 的强大之处。
## 总结 ## 总结
我们日常开发中使用并发的场景太多但是对并发内部的基本框架原理了解的人却不多。由于篇幅原因本文仅介绍了可重入锁ReentrantLock的原理和AQS原理希望能够成为大家了解AQS和ReentrantLock等同步器的“敲门砖”。 我们日常开发中使用并发的场景太多,但是对并发内部的基本框架原理了解的人却不多。由于篇幅原因,本文仅介绍了可重入锁 ReentrantLock 的原理和 AQS 原理,希望能够成为大家了解 AQS ReentrantLock 等同步器的“敲门砖”。
## 参考资料 ## 参考资料
- Lea D. The java. util. concurrent synchronizer framework[J]. Science of Computer Programming, 2005, 58(3): 293-309. - Lea D. The java. util. concurrent synchronizer framework[J]. Science of Computer Programming, 2005, 58(3): 293-309.
- 《Java并发编程实战》 - 《Java 并发编程实战》
- [不可不说的Java“锁”事](https://tech.meituan.com/2018/11/15/java-lock.html) - [不可不说的 Java“锁”事](https://tech.meituan.com/2018/11/15/java-lock.html)