文章目录
文章后续于https://github.com/zgkaii/CS-Study-Notes更新,欢迎批评指正!
1 AQS概述
AbstractQueuedSynchronizer,即队列同步器,一般简称AQS。它是构建锁或者其他同步组件的基础(如 Semaphore、CountDownLatch、ReentrantLock、ReentrantReadWriteLock),是 JUC 并发包中的核心基础组件。
它抽象了竞争的资源和线程队列,使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如上篇文章写的ReentrantLock与ReentrantReadWriteLock。除此之外,AQS还能构造出Semaphore,FutureTask(jdk1.7) 等同步器。
AQS 的主要使用方式是继承,子类通过继承同步器,并实现它的抽象方法来管理同步状态。
AQS 使用一个 int
类型的成员变量 state
来表示同步状态:
- 当
state > 0
时,表示已经获取了锁。 - 当
state = 0
时,表示释放了锁。
它提供了三个方法,来对同步状态 state
进行操作,并且 AQS 可以确保对 state
的操作是安全的:
#getState()
#setState(int newState)
#compareAndSetState(int expect, int update)
可以把AQS理解为抽象队列式的同步器,它有两种资源共享方式:独占|共享,子类负责实现公平|非公平,下面我们会详细讲到。
如何理解AQS与锁的关系呢?
其实很好理解,锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
2 AQS常用方法
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
private volatile int state;// 共享变量,使用volatile修饰保证线程可见性
同步状态state
通过 protected 类型的getState
,setState
,compareAndSetState
方法进行操作
// 返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
// CAS更新同步状态,该方法能够保证状态设置的原子性
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
自定义同步器时需要重写下面几个 AQS 提供的模板方法:
tryAcquire(int arg) // 独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获 // 取同步状态。成功则返回true,失败则返回false。
tryRelease(int arg) // 独占式释放同步状态。成功则返回true,失败则返回false。
tryAcquireShared(int arg)// 共享式获取同步状态,返回值大于等于0,则表示获取成功;否则,获取失败。
tryReleaseShared(int arg)// 共享式释放同步状态。成功则返回true,失败则返回false。
isHeldExclusively() // 当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占。
// 只有用到condition才需要去实现它。
AQS 还主要提供了如下方法:
acquire(int arg)
:独占式获取同步状态。如果当前线程获取同步状态成功,则由该方法返回;否则,将会进入同步队列等待。该方法将会调用可重写的#tryAcquire(int arg)
方法;#acquireInterruptibly(int arg)
:与#acquire(int arg)
相同,但是该方法响应中断。当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException 异常并返回。#tryAcquireNanos(int arg, long nanos)
:超时获取同步状态。如果当前线程在 nanos 时间内没有获取到同步状态,那么将会返回 false ,已经获取则返回 true 。#acquireShared(int arg)
:共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;#acquireSharedInterruptibly(int arg)
:共享式获取同步状态,响应中断。#tryAcquireSharedNanos(int arg, long nanosTimeout)
:共享式获取同步状态,增加超时限制。#release(int arg)
:独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒。#releaseShared(int arg)
:共享式释放同步状态。
从上面的方法看下来,同步器提供的模板方法基本上分为3类:
- 独占式获取与释放同步状态
- 共享式获取与释放同步状态
- 查询同步队列中的等待线程情况。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease
、tryAcquireShared-tryReleaseShared
中的一种即可。
以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后续动作。
但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock
。
2 AQS实现分析
2.1 同步队列
AQS 是依赖 CLH 队列锁来完成同步状态的管理。如果当前线程获取同步状态失败(锁)时,AQS 则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程;当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(FIFO双向队列)(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点信息。
属性类型与名称 | 描述 |
---|---|
int waitStatus | 等待状态(如CANCELLED=1、SIGNAL=-1、CONDITION=-2、PROPAGATE=-3、INITIAL=0) |
Node prev | 前驱节点(当节点加入同步队列时被设置,在尾部添加) |
Node next | 后继节点 |
Thread thread | 当前获取同步状态的线程 |
节点源码如下:
static final class Node {
// 表示该节点等待模式为共享式,通常记录于nextWaiter,
// 通过判断nextWaiter的值可以判断当前结点是否处于共享模式
static final Node SHARED = new Node();
// 表示节点处于独占式模式,与SHARED相对
static final Node EXCLUSIVE = null;
// waitStatus的不同状态
// 当前结点是因为超时或者中断取消的,进入该状态后将无法恢复
static final int CANCELLED = 1;
// 当前结点的后继结点是(或者将要)由park导致阻塞的,当结点被释放或者取消时,需要通过unpark唤醒后继结点
static final int SIGNAL = -1;
// 表明结点在等待队列中,结点线程等待在Condition上
// 当其他线程对Condition调用了signal()方法时,会将其加入到同步队列中
static final int CONDITION = -2;
// 下一次共享式同步状态的获取将会无条件地向后继结点传播
static final int PROPAGATE = -3;
volatile int waitStatus;
// 记录前驱结点
volatile Node prev;
// 记录后继结点
volatile Node next;
// 记录当前的线程
volatile Thread thread;
// 用于记录共享模式(SHARED), 也可以用来记录CONDITION队列
Node nextWaiter;
// 通过nextWaiter的记录值判断当前结点的模式是否为共享模式
final boolean isShared() {
return nextWaiter == SHARED;}
// 获取当前结点的前置结点
final Node predecessor() throws NullPointerException {
... }
// 用于初始化时创建head结点或者创建SHARED结点
Node() {
}
// 在addWaiter方法中使用,用于创建一个新的结点
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// 在CONDITION队列中使用该构造函数新建结点
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
// 记录头结点
private transient volatile Node head;
// 记录尾结点
private transient volatile Node tail;
2.1.1 入队
节点是构成同步队列的基础,同步器拥有首节点(Head)和尾节点(Tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部。同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect, Node update)
,它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。关联过程:(1)tail
指向新节点。(2)新节点的 prev
指向当前最后的节点。(3)当前最后一个节点的 next
指向当前节点。
入队逻辑是实现的 #addWaiter(Node)
方法,需要考虑并发的情况。它通过 CAS 的方式,来保证正确的添加 Node 。
private Node addWaiter(Node mode) {
// 新建节点。在创建的构造方法,`mode` 方法参数,传递获取同步状态的模式。