AQS 全称 AbstractQueuedSynchronizer,J.U.C 并发包中的核心基础组件之一,可用来构建锁和同步器。它底层用了 CAS 技术来保证操作的原子性,同时利用 FIFO 队列管理竞争锁的线程,ReentrantLock、CountDownLatch 等同步API 都是基于AQS实现的
本文目标
通过 ReentrantLock 为入口,深入理解 AQS
AQS 数据结构
// FIFO 队列的首节点
private transient volatile Node head;
// FIFO 队列的尾节点
private transient volatile Node tail;
// 当前的同步状态
private volatile int state;
static final class Node {
// 当前节点状态
volatile int waitStatus;
// 上一节点
volatile Node prev;
// 下一节点
volatile Node next;
// 每个Node对应一个线程
volatile Thread thread;
// 当前模式:独占/共享
Node nextWaiter;
}
Node 组成了我们刚刚说的 FIFO
这里只是简单给大家介绍一下,AQS的数据结构,别慌。可能有些参数你并不理解,后文我们会详细解释。我第一次看AQS的源码的时候,我也很懵逼。别着急,慢慢看,万一看懂了呢
直接看AQS的源码有点生硬,我们来通过 ReentrantLock 来理解AQS
ReentrantLock 如何应用AQS
来看一下类机构
ReentrantLock 的内部类 Sync 继承了 AQS,ReentrantLock完全依赖Sync 来实现锁。在Sync 基础上又衍生了,FairSync,NonFairSync 即公平锁和非公平锁
默认的时候,构建的是非公平锁,我们也可以指定 fair
来决定构建 Fair 还是 Nonfair
回忆 ReentrantLock 使用方法
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
这是JDK注释中的一个小栗子,通常来说我们用的比较多的方法,就是lock(), trylock(), unlock() 这几个。
OK. 那我们就先从 lock() 方法搞起
ReentrantLock.lock()
这里我抽出主要的代码用于演示,这里以非公平锁为例
第一步就不说了
第二步,通过compareAndSetState 尝试将 state 状态从0更新到1,这里用到就是java的 CAS(不了解CAS的同学可以Google一下)。这里ReentrantLock 利用 AQS 中的 state 变量做为锁的状态,0代表无锁,1代表当前有锁,如果CAS 更新state 成功,则说明当前线程获取锁成功,将独占线程设置为当前线程。
第三步,调用 acquire(1); 该方法是AQS 提供的,acquire 会尝试获取锁,再获取失败后,当前线程进入队列等待
这里也可以看出,NonfairSync 非常的霸道,他在调用lock方法后,就想尝试获取锁
AQS.acquire()
在第一次CAS尝试获取锁失败后,会执行到 acquire() 下面来分析一下该方法的逻辑
第一步 tryAcquire,该方法AQS 并没有提供具体实现,从图1可以看到,非公平锁的 tryAcquire() 调用的是 nonfairTryAcquire() ,具体代码在图3中
和 lock() 中的代码有点相似,首先是判断状态,如果无锁则尝试 CAS更新state,如果更新成功则代表获取锁成功。
然后 else if 这段的逻辑就是如果 “当前线程已经获取锁”,还可以继续更新state。这是啥意思?这就是为什么叫可重入锁,代表当前线程即使执行 lock(),还可以继续执行lock()
第二步,在tryAcquire 获取锁失败后,AQS 会尝试让当前线程作为一个Node入队(加入到队列的末尾),将当前线程构建为Node
对象,模式为“独占”。如果CAS入队失败,或者队列为空,调用enq,自旋入队(不断的尝试CAS入队,保证线程安全)。这一步具体代码参考图2
AQS.acquireQueued()
前两次抢占锁失败的线程会进入到AQS的 FIFO 队列中,队列中的节点按照“先进先出”的规则出队,通过自旋尝试获取锁
第一步,如果当前Node 的上一个节点为head,则调用tryAcquire 尝试获得锁
这里解释一下,为什么满足 “p == head” 的Node 有资格获得锁,因为在AQS的队列中,head 节点是无意义的。为什么这么说,可以看下图2中的
enq()
,初始化队列的时候,会new 一个Node 作为 head。再看图4中,如果第二个节点成功获取锁,该节点会升级为head。
第二步,shouldParkAfterFailedAcquire 从方法名我们可以分析出,该方法是判断当前线程再获取锁失败后,是否可以进入挂起状态。
在看shouldParkAfterFailedAcquire() 之前,先来了解一下 Node中waitStatus 的两个状态
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1; // 表示当前线程被取消
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1; // 表示后续线程需要挂起
具体代码在下面图5中
- 如果上一节点状态为
SIGNAL
,返回true,当前线程会进入挂起 - 如果上一节点状态为
CANCELLED
,则将这个节点从队列中移除 - waitStatus 为其他状态时,CAS 尝试将waitStatus 更新为
SIGNAL
第三步,如果shouldParkAfterFailedAcquire 返回true,则 parkAndCheckInterrupt 挂起当前线程
如果 acquireQueued() 方法执行过程中,如果抛出异常,入队失败,则调用cancelAcquire() 取消当前节点
总结
上面我们分析了 ReentrantLock 的部分源码,ReentrantLock 通过AQS提供的state 以及 FIFO,巧妙的实现了锁。其实AQS的源码远不止这些,还有很多值得思考的东西,后续还会为大家分享相关的知识。
最后我们通过一张图,再来加深一下本节的理解
如果觉得还不错欢迎点赞、转发。你的支持就是对我最大的帮助