Craig, Landin, and Hagersten - tenji/ks GitHub Wiki
CLH (Craig, Landin, and Hagersten)
一、基本概念
AQS 内部维护着一个 FIFO 的队列,即 CLH 队列。AQS 的同步机制就是依靠 CLH 队列实现的。CLH 队列是 FIFO 的双端双向队列,实现公平锁。头节点是一个获取同步状态成功的节点。线程通过 AQS 获取锁失败,就会将线程封装成一个 Node 节点,插入队列尾。当有线程释放锁时,后唤醒头节点的 next 节点(第二个节点)尝试占用锁。
为啥 AQS 内部要维护一个双向队列,而不是单向队列?
答:空间换时间,为了节约出队的时间,特别是高并发下会有频繁的入队出队操作。
假如你的队列是单向的如:Head -> N1 -> N2 -> Tail。出队的时候你要获取 N1 很简单,Head.next 就行了,入队你就麻烦了,你要遍历整个链表到 N2,然后 N2.next = N3; N3.next = Tail。入队的复杂度就是 O(n),而且 Tail 也失去他的意义。相反双向链表出队和入队都是 O(1) 时间复杂度。
二、队列结构
这里是基于 CAS(保证线程的安全)来设置尾节点的。
三、Node 对象
CLH 队列由 Node 对象组成,Node 是 AQS 中的内部类。
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;
// 节点在等待队列中,线程在等待在 Condition 上,其他线程对 Condition 调用 singnal() 方法后,该节点加入到同步队列中
static final int CONDITION = -2;
// 表示下一次共享式获取同步状态的时会被无条件的传播下去
static final int PROPAGATE = -3;
// 等待状态
volatile int waitStatus;
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
// 获取同步状态的线程
volatile Thread thread;
// 链接下一个等待状态
Node nextWaiter;
// 下面一些方法就不贴了
}
四、入列操作
如上图了解了同步队列的结构, 我们在分析其入列操作在简单不过。无非就是将 tail 指向新节点,新节点的 prev 指向队列中最后一节点(旧的 tail 节点),原队列中最后一节点的 next 节点指向新节点以此来建立联系,来张图帮助大家理解。
源码我们可以通过 AQS 中的以下两个方法来了解下:
4.1 addWaiter 方法
private Node addWaiter(Node mode) {
// 以给定的模式来构建节点, mode 有两种模式
// 共享式 SHARED, 独占式 EXCLUSIVE;
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 方式入列
enq(node);
return node;
}
先通过 addWaiter(Node node) 方法尝试快速将该节点设置尾成尾节点,设置失败走 enq(final Node node) 方法。
4.2 enq 方法
private Node enq(final Node node) {
// CAS自旋,直到加入队尾成功
for (;;) {
Node t = tail;
if (t == null) {
// 如果队列为空,则必须先初始化 CLH 队列,新建一个空节点标识作为 Header 节点,并将 tail 指向它
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 正常流程,加入队列尾部
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
通过“自旋”也就是死循环的方式来保证该节点能顺利的加入到队列尾部,只有加入成功才会退出循环,否则会一直循序直到成功。
上述两个方法都是通过 compareAndSetHead(new Node()) 方法来设置尾节点,以保证节点的添加的原子性。
五、出列操作
同步队列(CLH)遵循 FIFO,首节点是获取同步状态的节点,首节点的线程释放同步状态后,将会唤醒它的后继节点(next),而后继节点将会在获取同步状态成功时将自己设置为首节点,这个过程非常简单。如下图
设置首节点是通过获取同步状态成功的线程来完成的(获取同步状态是通过 CAS 来完成),只能有一个线程能够获取到同步状态,因此设置头节点的操作并不需要 CAS 来保证,只需要将首节点设置为其原首节点的后继节点并断开原首节点的 next(等待 GC 回收)应用即可。