一、 AQS框架
AQS定义了一套多线程访问共享资源的同步器框架
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 CLH 锁 (Craig, Landin, and Hagersten locks) 实现的。CLH 锁是对自旋锁的一种改进,是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系),暂时获取不到锁的线程将被加入到该队列中。
牢记该图!!!
AQS维护了一个共享资源state和一个FIFO的线程等待队列
1) state 设计
state 使用了 32bit int 来维护同步状态,state 使用 volatile 配合 cas 保证其修改时的原子性
2) 阻塞恢复设计
早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume那么 suspend 将感知不到
解决方法是使用 park & unpark 来实现线程的暂停和恢复
park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细park 线程还可以通过 interrupt 打断
3) 队列设计
使用了 FIFO 先入先出队列,并不支持优先级队列
设计时借鉴了 CLH 队列,它是一种单向无锁队列
二、源码
1. 节点状态
AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个结点(Node)来实现锁的分配。在 CLH 队列锁中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。
-
CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
-
SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
-
CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
-
PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
-
0:新结点入队时的默认状态。
注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。
2. acquire(int)
如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。
public final void acquire(int arg) {
// 1. 尝试获取资源
// 2. 获取失败创建新节点并将其加入等待队列尾部
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
2.1 tryAcquire(int)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
具体如何尝试获取资源由自定义同步器实现。注意这里并不是abstract方法,因为只有独享模式才需要实现此方法。
2.2 addWaiter(Node)
private Node addWaiter(Node mode) {
// 以mode模式构建节点,独占或者共享
Node node = new Node(Thread.currentThread(), mode);
// 尝试将node节点放到队尾
Node pred = tail;
if (pred != null) {
node.prev = pred;
//将tail更新为新加入的节点,即tail指向新入队节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 失败则通过enq入队
enq(node);
return node;
}
2.3 enq(Node)
private Node enq(final Node node) {
for (;;) {// 自旋
Node t = tail;
if (t == null) {
// 如果tail为空,初始化一个节点将其设为head
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 和addWaiter一样的流程
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
2.3 acquireQueued(Node,int)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; // 是否成功拿到资源
try {
boolean interrupted = false; // 是否被中断过
for (;;) {
final Node p = node.predecessor();
// 如果前驱是head,说明有资格尝试获取资源
if (p == head && tryAcquire(arg)) {
// 获取到资源后,该节点就是第一个节点,因此设为head
setHead(node);
// 将p节点从队列中断开
p.next = null; // help GC
failed = false; // 获取资源成功
return interrupted;
}
// 获取资源失败,尝试进入waiting
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
2.3 shouldParkAfterFailedAcquire(Node pred, Node node)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前驱节点的状态已经是SIGNAL了
return true;
if (ws > 0) {
// 前驱节点已取消。一直往前找,直到找到一个状态>0的节点,并将node作为该节点的下一节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 否则,将前驱的状态设为SIGNAL,这样可以在之后唤醒node
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
2.4 parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
2.5 总结
- 尝试获取资源,获取成功直接返回
- 获取资源失败,构建新节点并将其加入等待队列尾部
- 会依次尝试用两种方式入队
- 进入队尾后,acquireQueued()使线程在等待队列中休息,直到获取到资源后才返回。
- 自旋,在每一次旋转中
- 判断该节点是否是head的下一个节点,若是,尝试获取资源。
- 如果获取到资源,return。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 如果获取资源失败,尝试进入waiting状态
- 从该节点node的前驱开始,找到waitStatus<0的第一个节点,并将该节点作为其下一个节点
- 将前驱的waitStatus的状态设为SIGNAL
- park()将当前线程进入waiting状态
3. release(int)
public final boolean release(int arg) {
if (tryRelease(arg)) {
// 找到head节点,并唤醒head的后继节点
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
3.1 tryRelease(int)
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
3.2 unparkSuccessor(Node)
唤醒等待队列中最前面的那个未放弃节点
// 唤醒node的后继节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
// 如果下一个节点为空或者其waitStatus>0
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
// 从后往前寻找<=0的有效节点
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
这里需要注意两个地方:
- s被唤醒后,进入acquireQueued()if (p == head && tryAcquire(arg))的判断
- p为head
- p不为head。因为s已经是等待队列中最前边的那个未放弃线程,通过shouldParkAfterFailedAcquire()的调整,s必然会跑到head的next结点,下一次自旋p==head就成立)
- 然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()返回
- 为什么从后向前找,而不是从前往后找?
由于并发问题,addWaiter()入队操作和cancelAcquire()取消排队操作都会造成next链的不一致,而prev链是强一致的,所以这时从后往前找是最安全的。
而addWaiter()里每次compareAndSetTail(pred, node)之前都有node.prev = pred,即使compareAndSetTail失败,enq()会反复尝试,直到成功。一旦compareAndSetTail成功,该node.prev就成功挂在之前的tail结点上了,而且是唯一的,这时其他新结点的prev只能尝试往新tail结点上挂。