本文主要分析AQS设计原理及骨架源码,关于Condition及AQS实现的工具类放到以后的文章单独介绍,防止行文过长
3.5.1 简介
AQS(AbstractQueuedSynchronizer)提供了一套实现阻塞锁和相关线程同步器(FIFO wait queue)的基础框架,它完成了大部分工作,使得我们可以很容易的实现一个自己的线程同步逻辑。
AQS的核心是一个代表共享资源状态的变量state
和一个同步等待队列
。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* 同步状态,线程通过此变量获得锁(即访问共享资源)
*/
private volatile int state;
/**
* 一个FIFO的双端队列,没有获得锁的线程进入该队列等待
*/
private transient volatile Node head;
private transient volatile Node tail;
}
AQS同步示意图
一个线程能否访问共享资源取决于是否能通过CAS修改state至期望的值,如果失败则进入等待队列。如上图,一个线程和队列头后继节点的线程都可以竞争锁资源,根据策略不同可以实现公平/非公平锁,后文介绍。
3.5.2 共享状态变量
AQS通过volatile int state
表示共享资源的状态。这样自定义的同步器只需要设置state就可以控制线程的同步行为。AQS提供了getState
, setState
, compareAndSetState
方法来访问state
变量。
AQS是模版方法模式的设计,对外提供了以下主要方法,用于获取/释放访问权限:
// Exclusive
public final void acquire(int arg);
public final void acquireInterruptibly(int arg) throws InterruptedException;
public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException;
public final boolean release(int arg);
// Shared
public final void acquireShared(int arg);
public final void acquireSharedInterruptibly(int arg) throws InterruptedException;
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException;
public final boolean releaseShared(int arg)
而在内部,AQS已经实现了同步逻辑,自定义同步器只需实现state
语义即可,一般我们重写以下方法:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
- 一般只需tryAcquire/tryRelease,tryAcquireShared/tryReleaseShared其中一对即可,所以没有将方法定义为
abstract
. - isHeldExclusively返回当前线程是否正在独占资源,在用到Condition时使用
3.5.3 等待队列
以acquire(int arg)
为例:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
若tryAcquire
返回true,则获取资源成功直接返回,否则进入排队过程。
AQS的等待队列是一个CLH阻塞队列的变种,CLH队列是一个单链表队列,队列中每个节点自旋等待其前驱节点释放锁。如上图,AQS等待队列是一个双端链表,同样依赖前驱节点状态,不过与CLH lock标志不同。
3.5.4 源码解析
- acquire(int arg)
接着上面acquire
方法分析,里边就有一个if语句:
1. tryAcquire: 子类实现,CAS获取资源
2. addWaiter: 失败则创建当前线程对应的节点并入列, 独占模式
3. acquireQueued: 接着进入排队过程
4: 根据acquireQueued返回排队过程是否发生过中断,如果是,则通过selfInterrupt补充中断
第一步就直接获取资源(竞争锁),而不是排队,说明这是一个非公平锁机制,你是否能推断怎样实现公平锁?
/**
* 创建一个指定模式(Node.EXCLUSIVE|Node.SHARED)的节点并入列
*/
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;
}
}
// 失败(队列为空或CAS竞争失败)则通过enq调用无限重试入列
enq(node);
return node;
}
/** 自旋入列 */
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 队列为空,则先初始化头节点,以后获得资源的节点会变成头节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 下面代码是不是和上面方法快速尝试的那一次一模一样!
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
当addWaiter
返回后,当前线程对应Node节点已经在队列中了,接下来调用acquireQueued
进行排队等待。不过在这之前,我们先看一下Node
:
static final class Node {
/** 标示节点在共享模式下等待 */
static final Node SHARED = new Node();
/** 标示节点在排他模式下等待 */
static final Node EXCLUSIVE = null;
// 以下常量表示该节点所处状态
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;
// 链接在condition上等待的线程或表示节点为Node.SHARED节点
Node nextWaiter;
}
waitStatus
状态:
状态 | 值 | 含义 |
---|---|---|
CANCELLED | 1 | 已取消. 当超时或中断发生(若响应中断)时,变为该状态,不再竞争资源state |
0 | 0 | 新创建的节点或从condition队列中被唤醒的节点状态先设为0 |
SIGNAL | -1 | 指示后继节点等待当前节点唤醒.由后继节点在入队时设置 |
CONDITION | -2 | 指示节点等待在一个condition条件队列上.被唤醒后进入CLH等待队列竞争state |
PROPAGATE | -3 | 指示当前节点在共享模式下等待 |
在acquireQueued
中排队:
/**
* 等待排他的获取资源,不可中断,被acquire或condition的wait方法调用
* 返回这期间是否线程被中断过,如过是,该方法返回后通过selfInterrupt()调用补充中断
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 资源,直到成功
for (;;) {
final Node p = node.predecessor();
// 如果前驱节点是头节点,则尝试获取一次资源(因为入队过程可能头节点代表正在执行线程已经执行完)
// 如果成功,说明此时头节点已经释放了资源
// 或者头节点释放资源后唤醒的node节点
if (p == head && tryAcquire(arg)) {
// node拿到资源,变成了头节点
setHead(node); // head=node,node.thread=null,node.prev=null
p.next = null; // help GC (原来的头节点从链表中断开,出队列)
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && // 判断是否应该挂起当前线程
parkAndCheckInterrupt()) // 如果是,则挂起并在唤醒时返回中断状态
interrupted = true; // 期间发生了中断
}
} finally {
if (failed) // 因超时或中断(响应中断情况下)没有成功获得资源
cancelAcquire(node); // waitStatus变为CANCELED
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 已经设置其前驱节点状态为SIGNAL,可以安全park了
return true;
if (ws > 0) {
// 如果前驱节点取消了竞争资源,则一直往前查,直到第一个waitStatus<=0点节点为止并且链接在它后面
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 形成新的链表,中间已取消的节点从链表解链接,等待GC
pred.next = node;
} else {
// 此时waitStatus必定为0或PROPAGATE,将前驱节点状态设置为SIGNAL,但暂不park,在acquireQueued下一次循环中重试一次,如果还不行再park
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false; //返回false使acquireQueued进行下一次循环
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted(); //返回是否中断过
}
acquire流程如下图:
-
release(int arg)
public final boolean release(int arg) { if (tryRelease(arg)) {// 调用子类实现尝试释放资源 Node h = head; // 此时线程正在运行,h != null说明当前线程就是head, // waitStatus不可能为1(CANCELED)或-2(CONDITION) if (h != null && h.waitStatus != 0) //到这里waitStatus就只可能为-1(SIGNAL)或-3(PROPAGATE)了 unparkSuccessor(h);//唤醒后继节点线程 return true; } return false; }
/** 唤醒一个后继节点线程 */ private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) // 将节点状态设为0,就算失败也没关系,因为下面就会唤醒后继节点线程 // 如果是共享模式,进入该方法前ws已经被设为0了 compareAndSetWaitStatus(node, ws, 0); // s代表应该唤醒(未取消)的后继节点,初始就认为是该node的后继节点 Node s = node.next; // 如果node.next就是未取消的,则省去了这里遍历 if (s == null || s.waitStatus > 0) { // 不管s本身是null还是已取消状态,都将其设置为null,应为下面的if可能都不满足,后面unpark那个if判断会有问题 s = null; // 开始从尾节点向node遍历,找到最后一个(离node最近)未取消的节点 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) //如果找到了,则唤醒 LockSupport.unpark(s.thread); }
-
acquireShared(int arg)
public final void acquireShared(int arg) { // >0: 获取成功且还有剩余资源,=0: 获取成功,无剩余资源,<0: 获取失败 if (tryAcquireShared(arg) < 0) doAcquireShared(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(); if (p == head) {//其前驱节点已为头节点,尝试再获取一次 int r = tryAcquireShared(arg); if (r >= 0) { //获取成功 setHeadAndPropagate(node, r);//当前线程节点设置为头节点,并唤醒一个后继节点 p.next = null; // help GC,原头节点从队列脱离 if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && //同acquireQueued parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node); /* * 尝试唤醒同步队列中一个节点: * propagate > 0:有剩余资源可用 * h == null || h.waitStatus < 0:原head状态为SIGNAL(-1)或PROPAGATE(-3) * (h = head) == null || h.waitStatus < 0: 新head状态为SIGNAL(-1)或PROPAGATE(-3) * 或者原/新头节点为null,不知道要不要唤醒,那就唤醒一次 */ if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } }
-
releaseShared(int arg)
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
/** 共享锁由于可能多个线程在请求和释放资源,所以会有多个线程同时调用该方法 */ private void doReleaseShared() { // 循环直到所有可以获取到资源的线程被唤醒 for (;;) { Node h = head; // 队列非空且存在后继节点 if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { //如果状态为SIGNAL,循环直到改为0,别的线程进来就不会重复唤醒 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; unparkSuccessor(h);//唤醒后继节点 } //否则,将waitStatus设为PROPAGATE,这是一个过渡状态,使得别的线程进来看到该状态后,就知道正在唤醒其他线程节点,只需要等待唤醒过程完成就可以了 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; //除以上两种情况,就进入下面h == head判断,循环直到新线程获得资源变成head } if (h == head) // 如果头节点没有变,说明唤醒过程已经完成(已经没有要唤醒的节点了),退出循环 break; } }
-
cancelAcquire
/** 该方法在所有获取资源的方法中可能会调用(在park之前线程外部中断调用) */ private void cancelAcquire(Node node) { // Ignore if node doesn't exist if (node == null) return; node.thread = null; // 跳过所有已取消的前驱节点,最后得到的pred则是一个从node->head第一个未取消节点 Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; // predNext用于后面将其CAS替换为其他节点,即将已取消的节点从队列断开 Node predNext = pred.next; // 直接将节点状态置为已取消,不用判断之前的状态, // waitStatus是volatile修饰的,其他线程可以立刻看到此修改 node.waitStatus = Node.CANCELLED; // 如果当前节点是尾节点,只需将尾节点替换为上面找到的pred即可 if (node == tail && compareAndSetTail(node, pred)) { // 然后将其后继设为null,此时已取消节点从队列中断开了 compareAndSetNext(pred, predNext, null); } else { // 否则,node的后继节点需要链接到pred后面或着唤醒 int ws; if (pred != head && // 1.pred不是头节点,则应该继续排队 // 括号中是为了确保前驱的状态为SIGNAL,线程可以安全的继续park ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {// 这里判断pred线程正好也调用了cancelAcquire Node next = node.next; if (next != null && next.waitStatus <= 0) // 如果当前线程节点的后继不为null且不是已取消状态,将其链接到pred后面 // 此时已取消的节点也从队列断开了 compareAndSetNext(pred, predNext, next); } else {//2.如果pred是头节点或上面设置状态失败,则直接唤醒 unparkSuccessor(node); } node.next = node; // help GC } }
更多文章,见公-众-号