AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)使用acquire/release(模板)方法对外提供功能。使用一个双向队列和指向队列两端的head/tail指针来实现线程的有序排队,
ps:head指针指向的第一个node为傀儡node,不包含真实数据,通常对应正在运行的线程
1.void acquire(int)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
该方法首先调用被子类覆写的tryacquire方法,根据自定义逻辑判断资源(state,volatile变量)能否被获取。若成功,则相应线程直接往下执行;若失败则按照AQS中已经定义好的逻辑进行排队等候,包括以下步骤:
1.addWaiter(Node.EXCLUSIVE) 将当前线程作为一个独享节点加入AQS队列的尾部进行排队
2.acquireQueued 确保排队等待后,能在适当的时机被唤醒(head中的线程释放资源后唤醒后面node)
3.前两步执行时是不响应中断的,因此后续需要判断是否存在中断信号,若有则补上中断
2.tryAcquire(arg)
以可重入锁ReentrantLock的实现为例。ReentrantLock中有一个继承了AQS的抽象静态内部类Sync,该类中的nonfairTryAcquire方法如下,注意在最后设置state的值,防止重排序:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//state为0时代表资源可用,此时CAS设置state为目标值
if (c == 0) {
//直接尝试修改state,无视排队中的线程,非公平
//公平锁的实现会判断,是否有线程在排队.若有则当前线程进队排队,若没有才会尝试修改state
if (compareAndSetState(0, acquires)) {
//成功则设置当前线程占用资源
setExclusiveOwnerThread(current);
return true;
}
}
//若state不为0说明资源已被占用,查看是否当前线程占用
else if (current == getExclusiveOwnerThread()) {
//若是,则重入。并增加state的值(释放时,只有该值减小至0时资源才真正释放)
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//最后设置volatile变量,防止前面的代码被重排序到后面
setState(nextc);
return true;
}
return false;
}
3.addWaiter(Node.EXCLUSIVE)
将当前线程封装在互斥的node中,并将node设置到队尾
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速将自己作为尾节点添加进队列
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//失败则自旋添加
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 初始化头节点,head和tail均指向该节点,此时数据为空,后续获得资源且正在运行的线程会包含在内
if (compareAndSetHead(new Node()))
tail = head;
} else {
//自旋将自己设置为尾节点
node.prev = t;
//若这里切换线程,则下一步CAS会失败,因此若CAS成功,说明t以及当前节点的前继节点必然指向之前的尾节点
if (compareAndSetTail(t, node)) {
//若这里切换线程,t仍然指向尾节点,即使此时tail已经指向其他节点,也并不影响当前线程视角中尾节点与当前线程节点的连接
t.next = node;
return t;
}
}
}
}
4.acquireQueued
确保当前线程在队列中排队阻塞后,其前驱节点的状态为signal,节点在释放资源时会判断自己的状态,并以此决定是否唤醒后继节点
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);
}
}
5.shouldParkAfterFailedAcquire
尝试使当前节点前驱waitStatus为signal,尝试失败则返回false,返回至acquireQueued方法继续自旋
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 前驱节点waitStatus为signal,直接返回true,代表可以安全的阻塞。
*/
return true;
//下面操作始终返回false,即需要重新自旋尝试
if (ws > 0) {
/*
* 若前驱节点waitStatus为cancelled,从队列中断开该节点,并重复这一操作
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//尝试设置前驱节点waitStatus为signal
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
6.parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
//将当前线程阻塞到aqs对象上
LockSupport.park(this);
//被唤醒时返回线程的中断状态,若存在中断信号,后续会补上中断位的设置
return Thread.interrupted();
}
这里返回的中断位最终会在acquire方法中重新设置中断位,acquire方法的上层调用处可能会有类似while(!this.isInterrupted())的代码会用到中断位。(这里为何要重设中断位,执行全程不管中断位不行么?)
因为park方法在中断置位时会直接往下执行,没有阻塞的效果,且不会将中断位复位。因此当中断位置位时,park方法会直接返回,进而导致节点无限自旋。所以需要Thread.interrupted()方法将中断位复位。
另外,wait与park不同,中断位在wait前设置时,执行到wait之后会抛出(中断)异常;中断位在wait中设置时,并不能马上响应中断,必须等其他线程释放锁,当前线程才能从wait处继续执行,此时会抛出(中断)异常。无论哪种情况,抛出异常后都会走catch块代码,不会立即释放锁(除非将异常抛到上层)
7.cancelAcquire
acquireQueued方法循环中出现中断或者超时,会执行该方法
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//为后续CAS设置pred的next指针做准备(predNext为原始值)
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//确保pred状态为signal,连接当前节点的前后节点(非cancelled状态)
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//无法确保则直接唤醒后继节点,后续节点在被唤醒后会确保其pred节点为signal状态,然后再park
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
8.release
调用子类实现的tryRelease,若返回true,且头节点waitStaus值不为0,说明后面有节点等着被唤醒。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
9.unparkSuccessor
唤醒后继节点,这里唤醒后会再次在acquireQueued方法中循环自旋调用tryAcquire获取资源,若被唤醒的节点和head节点中间存在cancelled节点,会在当次循环中在shouldParkAfterFailedAcquire方法中从队列中移除这些节点,下次循环即可调用tryAcquire方法尝试获取资源。若此次获取失败了,说明可能是另一个线程在当前线程从阻塞中醒来之后、调用tryAcquire方法之前抢占了资源(非公平锁未排队)。
private void unparkSuccessor(Node node) {
//这里为何要清除waitStatus有点不明白,ws<0说明node并非被取消的节点,那么node应该是头节点,如果node的后继被唤醒后抢到了资源,那么node将出队;若没有抢到,node的后继在park前会将其前驱node再设置成signal。难道是在node的后继抢资源的过程中避免对其重复调用unpark?
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//从尾到头找到最后一个(离当前节点最近的)非cancelled的节点,并唤醒(中间可能有cancelled的节点)
Node s = node.next;
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);
}
唤醒后继之前要清除前驱节点的waitStatus,猜想应该是避免对同一后继节点重复调用unpark,这样会导致后继节点无法阻塞。如果对运行状态下的线程调用unpark,那么线程在第一次park自己的时候会消耗掉前面unpark提供的permit,继续往下运行(自旋),而不是阻塞自己。
10.doAcquireShared
与上述tryAcquire的主要区别是:成功抢到资源后不仅更新头节点,还要决定是否扩散共享状态,即唤醒后续共享状态的节点。涉及方法:setHeadAndPropagate
Node h = head; // Record old head for check below
setHead(node);
/*
若还有剩余资源,或者其他情况(暂时没看懂)
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//后续节点处于共享状态,则继续唤醒
if (s == null || s.isShared())
doReleaseShared();
}
11.doReleaseShared
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//与release不同,必须要先将node的waitStatus清0,也没看懂为何
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
//如果ws为0,说明后续没有要唤醒的节点,那么为什么还是设置h的状态为PROPAGATE?若后来有节点排队,那么后来的节点也会在park之前将h的状态设置为signal,不管h的状态是0还是PROPAGATE
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}