介绍
AQS是JUC包中鼎鼎有名的并发工具基础类,许多并发工具如ReentrantLock、Semaphore、CountDownLatch、线程池的Worker都是以其为基础实现的。因此了解其运行原理十分有必要,本文为总结性的文章,不会对源码做过多分析,旨在描述清楚AQS的运作原理。详尽的源码分析可看这位大佬的文章一行一行源码分析清楚AQS。
AQS是抽象队列同步器的简称。从他的名字我们可以了解到:
- 抽象:只实现了一些主要逻辑,其余方法由子类实现
- 队列:使用FIFO队列存储数据(线程节点)
- 同步:实现了同步
AQS是一个可以用来构建锁和同步器的框架,使用他可以简单高效的构造出应用广泛的同步器,如上面提到的几个。
AQS提供了独占模式和共享模式的同步方式。本文分析AQS的独占锁篇,将以ReentrantLock为例。
AQS结构
基础字段
// 头结点,你直接把它当做 当前持有锁的线程
private transient volatile Node head;
// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
private transient volatile Node tail;
// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁
// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
private volatile int state;
// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread;
接下来我们看AQS中的队列是什么样子的:
注意队列中不包含head节点!!。这张图很重要,请记住它!
可以看到阻塞队列是以双向链表的形式构建的,我们来看看Node的结构
static final class Node {
// 标识节点当前在共享模式下
static final Node SHARED = new Node();
// 标识节点当前在独占模式下
static final Node EXCLUSIVE = null;
// ======== 下面的几个int常量是给waitStatus用的 ===========
/** waitStatus value to indicate thread has cancelled */
// 代码此线程取消了争抢这个锁
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
// 表示当前node的后继节点对应的线程需要被唤醒
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
// 表示当前节点在等待某一条件,条件成立才能被唤醒
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
// 共享模式下,表示还有资源可用,需要继续唤醒head的后继节点
static final int PROPAGATE = -3;
// =====================================================
// 取值为上面的1、-1、-2、-3,或者0(以后会讲到)
// 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待
volatile int waitStatus;
// 前驱节点的引用
volatile Node prev;
// 后继节点的引用
volatile Node next;
// 这个就是节点对应的线程
volatile Thread thread;
// Condition等待队列里下一个等待条件的结点
// 主要用在ReentrantLock的Condition中
Node nextWaiter;
}
注意:通过Node我们可以实现两个队列,一是通过prev和next实现CLH队列(线程同步队列,双向队列),二是nextWaiter实现Condition条件上的等待线程队列(单向队列),这个Condition主要用在ReentrantLock类中。
主要方法
AQS是基于模板方法模式设计的,也就是有一些方法必须由子类实现。主要有
-
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
-
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
-
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
-
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
-
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
也就是说,如果我们要构建自己的同步器,那么我们就要实现上面的方法,也可以理解为我们可以通过上面的方法定义自己的同步器规则。当然不用全部都实现,根据自己的需要选择独占方式或共享方式实现就行。
内部流程
接下来说说AQS为我们做了什么,他为我们实现了获取资源和释放资源的主要逻辑,也就是当抢不到资源时,何时需要挂起线程,何时需要唤醒线程。这些AQS帮我们实现了,我们只需要重写方法,定义自己的同步器规则就行。
我们以ReentrantLock的公平锁为例,我们知道ReentrantLock是可重入锁,那他是怎么实现的呢?ReentrantLock重写了tryAcquire和tryRelease,也就是可重入的实现规则是在这两个方法中定义的。
大家先看一下下面的流程图,蓝色方框的就是子类重写的方法,黑色方框的是AQS中的方法。
AQS的代码很巧妙,大家一定要仔细看。
看完流程,接下来总结下每个方法的流程:
acquire
先通过tryAcquire尝试获取锁,成功了就直接返回;否则就调用acquireQueued
将其加入到阻塞队列中,再判断是否需要将其挂起。addWaiter(Node mode)
将线程包装成Node节点,通过CAS插入到阻塞队列尾部。第一次CAS失败会调用enq(node)
自旋CAS插入到队列尾部,此次一定保证插入成功。enq会初始化头结点。acquireQueued( node ,1 )
真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了。返回值是线程是否被中断过。shouldParkAfterFailedAcquire
判断线程是否需要挂起,如果前驱节点状态正常,则将其挂起;否则修正其前驱节点到正常的节点。parkAndCheckInterrupt
挂起线程并检查中断状态。release
释放锁资源。会先调用tryRelease,如果成功则唤醒head节点后继线程。