首先,为什么要理解AQS???
因为同步组件(这里不仅仅指锁,还包括CountDownLatch等)的实现依赖于同步器AQS,即AQS是同步组件实现的核心部分。
那么,AQS到底是什么呢???
AQS(AbstractQueuedSynchronizer),简称同步器,是用来构建锁和其它同步组件的基础框架。AQS的组成可以理解如下图:
要想掌握AQS的底层实现,我们就要学习这些模板方法,首先我们就得了解AQS中的同步队列是个什么样的数据结构,因为同步队列是AQS对同步状态管理的基石。
当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。AQS中的同步队列则是通过链式方式进行实现。在AQS有一个静态内部类Node,这是我们同步队列的每个具体节点。在这个类中有如下属性:
现在我们知道了节点的数据结构类型,并且每个节点拥有其前驱和后继节点,很显然这是一个双向队列。AQS实际上通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,获取锁成功的线程进行出队,释放锁时对同步队列中的线程进行通知等核心方法。其示意图如下:
调用lock()方法是获取独占锁,获取失败就将当前线程加入同步队列,成功则线程执行。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
`
使用CAS来尝试将同步状态改为1,若成功则将同步状态持有线程置为当前线程。否则将调用AQS提供的aquire()方法。
public final void acquire(int arg) {
// 再次尝试获取同步状态,如果成功则方法直接返回
// 如果失败则先调用addWaiter()方法再调用acquireQueued()方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
使用acquire方法再次获取同步状态,若成功则直接返回,若失败就将当前线程加入同步队列,再排队继续获取锁。
上述过程可理解如下图:
接下来我们就分别来研究一下addWaiter()方法和acquireQueued()方法。
private Node addWaiter(Node mode) {
// 将当前线程包装称为Node类型
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 当前尾节点不为空
if (pred != null) {
// 将当前线程以尾插的方式插入同步队列中
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 当前尾节点为空或CAS尾插失败
enq(node);
return node;
}
通过上述的代码我们可以发现,addWaiter()方法的流程图如下:
通过上述流程图我们可以发现,enq()方法的执行结果一定是成功的,那么原因是什么呢?我们来分析一下enq()的源码:
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;
}
}
}
}
通过上述代码我们可以发现,enq()的流程图如下:
现在我们已经很清楚获取独占式锁失败的线程包装成Node然后插入同步队列的过程了,那么我们就要清楚在同步队列中的结点(线程)如何来保证自己能够有机会获得独占式锁了,来分析一下acquireQueued()方法。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获得当前节点的前驱节点
final Node p = node.predecessor();
// 当前节点能否获取独占式锁
// 当前节点的前驱节点是头结点
if (p == head && tryAcquire(arg)) {
// 队列头指针指向当前节点
setHead(node);
// 释放前驱节点
p.next = null; // help GC
failed = false;
return interrupted;
}
// 获取同步状态失败,线程进入等待状态等待获取独占锁
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
通过上述代码我们可以发现,acquireQueued()方法的流程图如下:
独占式锁的释放调用unlock()方法,而该方法实际调用了AQS的release方法。
unlock()方法:
public void unlock() {
sync.release(1);
}
release()方法:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
通过上述代码我们可以发现,独占式锁的释放过程如下:
独占式锁的总结:
1. 线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试;
2. 线程获取锁是一个自旋的过程,当且仅当 当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞;
3. 释放锁的时候会唤醒后继节点;
总体来说:在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。
可响应中断式锁可调用方法lock.lockInterruptibly(),而该方法其底层会调用AQS的acquireInterruptibly方法。
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
// 线程获取锁失败
doAcquireInterruptibly(arg);
}
在获取同步状态失败后就会调用doAcquireInterruptibly方法:
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
// 将节点插入到同步队列中
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
// 获取锁出队
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 线程中断异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
上述代码与acquire方法逻辑几乎一致,唯一的区别是当parkAndCheckInterrupt返回true时(即线程阻塞时)该线程被中断,代码抛出被中断异常。
通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果,该方法会在三种情况下才会返回:
1. 在超时时间内,当前线程成功获取了锁;
2. 当前线程在超时时间内被中断;
3. 超时时间结束,仍未获得锁返回false。
该方法会调用AQS的方法tryAcquireNanos()。
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
// 实现超时等待的效果
doAcquireNanos(arg, nanosTimeout);
}
最终是靠doAcquireNanos方法实现超时等待的效果:
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
// 1.根据超时时间和当前时间计算出截止时间
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
// 2.当前线程获得锁出队列
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 3.1 重新计算超时时间
nanosTimeout = deadline - System.nanoTime();
// 3.2 已经超时返回false
if (nanosTimeout <= 0L)
return false;
// 3.3 线程阻塞等待
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
// 3.4 线程被中断抛出被中断异常
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
通过上述代码我们可以发现,doAcquireNanos方法的程序流程如下:
程序逻辑同独占锁、可响应中断式获取基本一致,唯一的不同在于获取锁失败后,对超时时间的处理上。
注:上述内容仅为自己在学习过程中的理解,如有不足之处,请指正。