本文基于jdk1.8
一.我是谁???
AbstractQueuedSynchronizer 简称AQS,翻译过来是抽象队列同步器,是多线程访问共享资源的基类。
CountDownLatch,ReentrantLock内部都有它的影子。
AQS使用的等待队列是“CLH”(Craig、Landin 和 Hagersten)锁定队列的变体。CLH 锁通常用于自旋锁。
二. AQS基石
node作为AQS的基石,关键属性分类:
- 有前后指针,表示最后组成一个双向链表
- 支持共享和独占两种模式获取资源
- waitStatus等待状态,各个状态的流转包含了核心处理逻辑
static final class Node {
// 标识共享模式
static final Node SHARED = new Node();
// 标识独占模式
static final Node EXCLUSIVE = null;
// 标识waitStatus当前线程已取消
static final int CANCELLED = 1;
// 标识waitStatus当前线程的后继线程需要唤醒
static final int SIGNAL = -1;
// 标识waitStatus当前线程等待条件
static final int CONDITION = -2;
// 标识waitStatus下一个acquireShared 应该无条件传播
static final int PROPAGATE = -3;
// 等待状态
volatile int waitStatus;
// 前继节点
volatile Node prev;
// 后继节点
volatile Node next;
...
}
三.acquire-分果果
acquire是独占模式下获取资源的方法,流程如下:
- 获取资源,获取成功返回
public final void acquire(int arg) {
// tryAcquire尝试获取资源,子类实现
if (!tryAcquire(arg) &&
// addWaiter加入等待队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 自我中断,如果曾经中断过,这里补偿性的进行中断操作
selfInterrupt();
}
- 获取资源失败,加入等待队列
private Node addWaiter(Node mode) {
// 构建一个节点
Node node = new Node(Thread.currentThread(), mode);
// 获取tail尾节点
Node pred = tail;
// 尾节点不存在,则说明是首次添加节点
if (pred != null) {
node.prev = pred;
// CAS操作进行设置当前节点为尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 入队操作
enq(node);
return node;
}
node.prev = pred和pred.next = node能否替换位置?
上文中采用尾插,如果调换了位置,在并发操作时,pred.next 指向可能不正确。
enq方法用于初始化空的头尾节点;初始化过的节点,不断尝试CAS替换当前节点为尾节点。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 首次插入节点,初始化一个空的头、尾结点
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// CAS操作进行设置当前节点为尾节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- 反复竞争获取资源,直到获取成功返回
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);
}
}
shouldParkAfterFailedAcquire方法不断尝试更新前置节点为SIGNAL状态,然后返回true。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// SIGNAL表示当前置节点释放锁,会唤醒当前节点,故当前可以放心中断了
if (ws == Node.SIGNAL)
return true;
// 在node状态中,大于0的状态只有CANCELLED 取消
// 故这里不管获取不处于取消状态的前置节点,并且设置该节点的后置节点为当前节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 这里CAS更改前置节点状态为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
注意:parkAndCheckInterrupt这个方法,当线程调用LockSupport.park会堵塞线程;如果发生中断,会返回之前的中断状态,后续用于补偿性的自我中断。
private final boolean parkAndCheckInterrupt() {
// 堵塞线程
LockSupport.park(this);
// interrupted这个会返回之前中断状态,并初始化中断状态为false
return Thread.interrupted();
}
这里为何要使用Thread.interrupted恢复状态?
如果不恢复默认中断状态,下次再进入这个方法 LockSupport.park是无效的。
对于Thread中断不是很清楚的同学,可以参考这篇文章。
四.release-还餐盘
1.尝试释放锁,直接返回结果(随缘返回,能释放就释放)
public final boolean release(int arg) {
// tryRelease释放锁,子类实现
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒下一个节点
unparkSuccessor(h);
return true;
}
return false;
}
唤醒后继可用节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果当前处于SIGNAL状态,设置状态为默认值
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取后置节点
Node s = node.next;
// 后置节点null或者处于取消状态,寻找可用的后置节点
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);
}