一、简介
AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)是用来构建锁或者其他同步 组件(信号量、事件等)的基础框架类。JDK中许多并发工具类的内部实现都依赖于AQS,如ReentrantLock, Semaphore, CountDownLatch等等。学习AQS的使用与源码实现对深入理解concurrent包中的类有很大的帮助。
AQS的主要使用方式是继承它作为一个内部辅助类实现同步原语,它可以简化你的并发工具的内部实现,屏蔽同步状态管理、线程的排队、等待与唤醒等底层操作。
二、实现思路
AQS内部维护一个CLH队列来管理锁。
- 线程会首先尝试获取锁,如果失败,则将当前线程以及等待状态等信息包成一个Node节点加到同步队列 里。
- 接着会不断循环尝试获取锁(条件是当前节点为head的直接后继才会尝试),如果失败则会阻塞自己,直至被唤醒。
- 而当持有锁的线程释放锁时,会唤醒队列中的后继线程。
下面列举JDK中几种常见使用了AQS的同步组件:
- ReentrantLock: 使用了AQS的独占获取和释放,用state变量记录某个线程获取独占锁的次数,获取锁时+1, 释放锁时-1,在获取时会校验线程是否可以获取锁。
- Semaphore: 使用了AQS的共享获取和释放,用state变量作为计数器,只有在大于0时允许线程进入。获 取锁时-1,释放锁时+1。
- CountDownLatch: 使用了AQS的共享获取和释放,用state变量作为计数器,在初始化时指定。只要state 还大于0,获取共享锁会因为失败而阻塞,直到计数器的值为0时,共享锁才允许获取,所有等待线程会 被逐一唤醒。
三、 API简介
AQS设计基于模板方法模式,开发者需要继承同步器并且重写指定的方法,将其组合在并发组件的实现中,调 用同步器的模板方法,模板方法会调用使用者重写的方法。
通过上面的AQS大体思路分析,我们可以看到,AQS主要做了三件事情:
- 同步状态的管理,主要是通过state变量。
- 线程的阻塞和唤醒,通过LockSupport.park和unpark方法 。
- 同步队列的维护, 双向链表 。
下面三个protected final方法是AQS中用来访问/修改同步状态的方法:
① int getState()方法。
//返回同步状态的当前值。该操作具有易失性读的内存语义。返回当前状态值
protected final int getState() {
return state;
}
② void setState(int newState)方法。
//设置同步状态的值。
//该操作具有volatile写的内存语义。新状态值
protected final void setState(int newState) {
state = newState;
}
③ boolean compareAndSetState(int expect, int update)方法。
//如果当前状态值等于期望值,则自动将同步状态设置为给定的更新值。该操作具有易失性读写的内存语义。
protected final boolean compareAndSetState(int expect, int update) {
//利用unsafe对象来进行CAS操作
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
在自定义基于AQS的同步工具时,我们可以选择覆盖实现以下几个方法来实现同步状态的管理,以下方法在 AQS中没有实现, 在AQS中会抛出UnsupportedOperationException异常,即把获取和释放的具体逻辑由子类来实现(主要是对state 变量的操作)。
方法 | 描述 |
---|---|
boolean tryAcquire(int arg) | 试图获取独占锁 |
boolean tryRelease(int arg) | 试图释放独占锁 |
int tryAcquireShared(int arg) | 试图获取共享锁 |
boolean tryReleaseShared(int arg) | 试图释放共享锁 |
boolean isHeldExclusively() | 当前线程是否获得了独占锁 |
AQS本身将同步状态的管理用模板方法模式都封装好了,以下列举了AQS中的一些模板方法:
方法 | 描述 |
---|---|
void acquire(int arg) | 获取独占锁。会调用tryAcquire方法,如果未获取成功,则会进入同步队列等待 |
void acquireInterruptibly(int arg) | 响应中断版本的acquire |
boolean tryAcquireNanos(int arg,long nanos) | 响应中断+带超时版本的acquire |
void acquireShared(int arg) | 获取共享锁。会调用tryAcquireShared方 法 |
void acquireSharedInterruptibly(int arg) | 响应中断版本的acquireShared |
boolean tryAcquireSharedNanos(int arg,long nanos) | 响应中断+带超时版本的acquireShared |
boolean release(int arg) | 释放独占锁 |
boolean releaseShared(int arg) | 释放共享锁 |
Collection getQueuedThreads() | 获取同步队列上的线程集合 |
上面看上去很多方法,其实从语义上区分就是获取和释放,从模式上区分就是独占式和共享式,从中断相应上来看就是支持和不支持。AQS为在独占模式和共享模式下获取锁分别提供三种获取方式:不响应线程中断获取,响应线程中断获取,设 置超时时间获取。
四、源码解析
嵌套类Node的定义
static final class Node {
//表示当前线程以共享模式持有锁
static final Node SHARED = new Node();
//表示当前线程以独占模式持有锁
static final Node EXCLUSIVE = null;
//表示当前线程已取消获取锁
//因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。
// 处于这种状态的结点会 被踢出队列,被GC回收;
static final int CANCELLED = 1;
//表示后继节点的线程需要运行
static final int SIGNAL = -1;
//表示当前节点在条件队列中排队,因为等待某个条件而被阻塞
static final int CONDITION = -2;
//表示后继节点可以直接获取锁,使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条 件传播
static final int PROPAGATE = -3;
//表示当前节点的等待状态
volatile int waitStatus;
//表示同步队列的前继节点
volatile Node prev;
//表示 同步队列 的后继节点
volatile Node next;
//表示当前节点持有的线程的引用
volatile Thread thread;
//表示 条件队列 中的后继结点
Node nextWaiter;
//如果节点在共享模式下等待,则返回true。
//当前结点状态是否是共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回当前节点的前继节点
final Node predecessor() throws NullPointerException {
Node p = prev;
//若当前节点的前继节点为空,则抛出异常
if (p == null)
throw new NullPointerException();
else
return p;
}
//用于建立初始标头或共享标头
Node() { // Used to establish initial head or SHARED marker
}
//构造方法2, 默认用这个构造方法
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//构造方法3, 只在条件队列中用到
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
变量说明
//序列化序号,用来对对象进行反序列化时的判断
private static final long serialVersionUID = 7373984972572414691L;
// 等待队列的头,延迟初始化。
// 除了初始化之外,它只通过setHead方法进行修改。
// 注意:如果head存在,则保证不会取消它的等待状态。
private transient volatile Node head;
//等待队列的尾部,延迟初始化。
// 仅通过方法enq修改以添加新的等待节点。
private transient volatile Node tail;
//当前节点的同步状态
private volatile int state;
//以纳秒为单位的旋转速度要比以时间为单位的停车速度快。
// 粗略的估计就足以在非常短的超时情况下提高响应能力。
static final long spinForTimeoutThreshold = 1000L;
同步字段state的取值
英文 | 取值 | 含义 |
---|---|---|
CANCELLED | 1 | 因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞 争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会 被踢出队列,被GC回收。 |
SIGNAL | -1 | 表示这个结点的继任结点被阻塞了,到时需要通知它。 |
CONDITION | -2 | 表示这个结点在条件队列中,因为等待某个条件而被阻塞。 |
PROPAGATE | -3 | 使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条件传播。 |
None of the above | 0 | 新结点会处于这种状态。 |
获取独占锁的实现
独占锁的获取是通过acquire(int arg)方法实现的,源码如下所示:
//获取独占锁,对中断不敏感。
public final void acquire(int arg) {
//首先尝试获取一次锁,如果成功,则返回;
if (!tryAcquire(arg) &&
/**
* addWaiter方法在队列中添加一个节点,并且返回新添加的节点
* acquireQueued方法在队列中会检测是否为head的直接后继,并尝试获取锁,
* 如果获取失败,则会通过LockSupport阻塞当前线程
* 直至被释放锁的线程唤醒或者被中断,随后再次尝试获取锁,如此反复。
*/
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//中断当前线程
selfInterrupt();
}
① 其中 tryAcquire(arg) 方法需要子类去实现,作用是尝试获取独占锁。
//尝试获取独占锁
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
② addWaiter(Node.EXCLUSIVE)方法的作用是在同步队列中的尾部添加一个节点。
//在队列中添加一个节点
//此方法是将新节点添加进同步队列的方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速查询;如果失败,备份到完整的询问
Node pred = tail;
//判断尾节点是否为空,即判断同步队列是否含有元素
if (pred != null) {
node.prev = pred;
//如果同步队列不为空,就通过CAS将新的节点设置为尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//若同步队列为空,就进入enq方法进行空队列的元素的添加
enq(node);
return node;
}
/** enq(node) 方法如下所示 */
//将节点插入队列,必要时进行初始化
//通过 循环+CAS 在队列中成功插入一个节点后返回
//此方法是在队列为空时才进行操作的
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//判断尾节点是否为空,即判断队列中是否有元素
if (t == null) { // Must initialize
//如果没有元素,就将一个节点添加到队列中
// 然后将这个新加入的节点设置为头节点和尾节点,并且进行第二次循环
if (compareAndSetHead(new Node()))
tail = head;
} else {
//将新添加到队列的节点的前缀节点设置为刚刚新加入队列的那个节点
node.prev = t;
//通过CAS将需要添加到队列的节点设置为尾节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
③ 而acquireQueued(final Node node, int arg)方法是让同步队列的第一个元素的节点获取锁。
//在队列中的节点通过此方法获取锁,对中断不敏感
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取当前节点的前继节点
final Node p = node.predecessor();
//1.判断当前节点的前驱节点是否为头节点
//2.如果是就尝试获取锁
if (p == head && tryAcquire(arg)) {
//如果锁获取成功就将当前节点设置为head节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果未成功获取锁则根据前驱节点判断是否要阻塞。
//如果阻塞过程中被中断,则置interrupted标志位为true。
//shouldParkAfterFailedAcquire方法在前驱状态不为SIGNAL的情况下都会循环重试获取锁。
//parkAndCheckInterrupt方法会阻塞线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果锁获取失败则取消获取
if (failed)
cancelAcquire(node);
}
}
④ 最后的selfInterrupt()方法则是中断当前线程。
//中断当前线程。
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
在非公平锁的模式下一个线程在进入同步队列之前会尝试获取两遍锁,如果获取成功则不进入同步队列排队, 否则才进入同步队列排队,独占锁的获取的大概流程如下图所示:
释放独占锁的实现
独占锁的释放是通过release(int arg)方法实现的,源码如下所示:
//释放独占锁的实现
public final boolean release(int arg) {
//先尝试释放锁
if (tryRelease(arg)) {
Node h = head;
//如果释放锁成功,就判断节点是否为空并且等待状态不等于0
if (h != null && h.waitStatus != 0)
//如果满足条件就唤醒head节点的后继节点
unparkSuccessor(h);
return true;
}
return false;
}
/** unparkSuccessor(h)方法如下所示 */
//唤醒node的后继节点(如果存在的话)。
private void unparkSuccessor(Node node) {
//获取节点的等待状态
int ws = node.waitStatus;
// 尝试将node的等待状态置为0,这样的话,后继争用线程可以有机会再尝试获取一次锁。
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//获取当前节点的后继节点
Node s = node.next;
// 第二个if表示:这里的逻辑就是如果node.next存在并且状态不为取消,则直接唤醒s即可
// 第一个if否则:需要从tail开始向前找到node之后最近的非取消节点。
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
整个release做的事情就是 :
- 调用tryRelease 尝试释放锁。
- 如果tryRelease返回true也就是独占锁被完全释放,唤醒后继线程。
获取共享锁的实现
与获取独占锁的实现不同的关键在于,共享锁允许多个线程持有。 如果需要使用AQS中共享锁,在实现tryAcquireShared方法时需要注意,返回负数表示获取失败,返回0表示成功。
//获取共享锁
public final void acquireShared(int arg) {
//如果需要使用AQS中共享锁
// 在实现tryAcquireShared方法时需要注意,返回负数表示获取失败;返回0表示成功
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
① 其中 tryAcquireShared(arg) 方法需要子类去实现,作用是尝试获取共享锁。
//尝试获取共享锁
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
② 而doAcquireShared(int arg)方法表示以共享不可中断模式获取锁。
//以共享不可中断模式获取锁
private void doAcquireShared(int arg) {
//将共享锁节点添加进同步队列中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取当前节点的前驱节点
final Node p = node.predecessor();
//如果当前节点的前驱节点是head节点
if (p == head) {
//尝试获取共享锁
int r = tryAcquireShared(arg);
//大于等于0代表获取锁成功
//一旦共享获取成功,设置新的头结点,并且唤醒后继线程
if (r >= 0) {
/**
* 这个函数做的事情有两件:
* 1. 在获取共享锁成功后,设置head节点
* 2. 根据调用tryAcquireShared返回的状态以及节点本身的等待状态来判断是否要需要唤醒后继线程
*/
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//shouldParkAfterFailedAcquire根据前驱节点中的waitStatus来判断是否需要阻塞当前线程。
//阻塞当前线程并且检查当前线程是否被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 该方法实现某个node取消获取锁,取消正在进行的获取尝试
cancelAcquire(node);
}
}
释放共享锁的实现
共享锁的释放是通过releaseShared(int arg)方法实现的,源码如下所示:
//释放共享锁
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
① 其中 tryReleaseShared(arg) 方法需要子类去实现,作用是尝试释放共享锁。
//尝试释放共享锁
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
② 而doReleaseShared()方法表示唤醒下一个线程或者设置传播状态。
/**
* 这是共享锁中的核心唤醒函数,主要做的事情就是唤醒下一个线程或者设置传播状态。
* 后继线程被唤醒后,会尝试获取共享锁,如果成功之后,则又会调用setHeadAndPropagate,将唤醒传播下去。
* 这个函数的作用是保障在acquire和release存在竞争的情况下,保证队列中处于等待状态的节点能够有办法被唤醒
*/
private void doReleaseShared() {
/**
* 以下的循环做的事情就是,在队列存在后继线程的情况下,唤醒后继线程;
* 或者由于多线程同时释放共享锁由于处在中间过程,读到head节点等待状态为0的情况下,
* 虽然不能unparkSuccessor,但为了保证唤醒能够正确稳固传递下去,设置节点状态为PROPAGATE。
* 这样的话获取锁的线程在执行setHeadAndPropagate时可以读到PROPAGATE,从而由获取锁的线程去释放后继等待线程
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果节点的状态为SIGNAL
if (ws == Node.SIGNAL) {
//就将SIGNAL变为0,如果CAS失败,则进入unparkSuccessor,相当与释放锁
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//唤醒node的后继节点(如果存在的话)。
unparkSuccessor(h);
}
//如果节点的状态不为SIGNAL,就将节点状态变为PROPAGATE
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
五、总结
当然AQS不止是这些方法,还有包括其他的可中断或者响应时间相关的方法,这里我就不全部进行介绍。总的来说,读AQS还是需要花费一定的时间和精力才能对它有所理解。慢慢来吧,一步一个脚印。