1 概述
了解过JUC的源码,我们就可以知道JUC下面很多工具的实现都是依靠AQS,而AQS中用于保存等待线程的队列就是CLH。CLH是一个FIFO的队列。队列的每一个节点都是一个Node对象。当前线程获取同步状态失败的时候就会进入CLH队列。而当同步状态被释放的时候会通知首节点再次去获取同步状态。
2 Node节点
首先我们来看看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;
/** 节点再等待队列中,节点的线程将会处于Condition等待状态,只有其他线程调用了Condition
* 的signal()后,该节点由等待队列进入到同步队列,尝试获取同步状态
*/
static final int CONDITION = -2;
/**
* 表示下一次共享式同步状态获取将会无条件传播下去
*/
static final int PROPAGATE = -3;
/**
* 等待状态:
* SIGNAL: 表示这个节点的后继节点被阻塞,到时需要通知它。
* CANCELLED: 由于中断和超市,这个节点处于被删除的状态。处于被删除状态的节点不会转
* 换成其他状态,这个节点将被踢出同步队列,被GC回收。
* CONDITION: 表示这个节点再条件队列中,因为等待某个条件而被阻塞。
* PROPAGATE: 使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条件
* 传播.
* 0: 新节点会处于这种状态。
*
*/
volatile int waitStatus;
/**
* 队列中,节点的前一个节点
*/
volatile Node prev;
/**
* 节点的后继节点.
*/
volatile Node next;
/**
* 节点所拥有的线程.
*/
volatile Thread thread;
/**
* 条件队列中,节点的下一个等待节点.
*/
Node nextWaiter;
/**
* 判断节点时候是共享模式.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 获取前一个节点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
针对节点中的每个属性所代表的意思,已经在注释中给出了详细的解释。下面我们来看看CLH同步队列结构图。
3 入列
通过CLH同步队列结构图我们基本可以猜到,CLH同步队列入列是怎么回事。就是将当前同步队列的最后一个节点的next指向添加的节点,并将添加节点的prev指向最后一个节点。同时需要将tail指向新添加的节点。下面我们来查看一下具体的方法addWaiter(Node mode)的源码。
private Node addWaiter(Node mode) {
//新建node
Node node = new Node(Thread.currentThread(), mode);
//快速尝试添加尾节点
Node pred = tail;
if (pred != null) {
node.prev = pred;
//使用cas设置尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//循环设置尾节点
enq(node);
return node;
}
在addWaiter方法中我们会尝试获取尾节点并进行尾节点的设置,如果成功就直接返回,如果没有成功就调用enq(final Node node)进行设置,下面我们来看看enq(final Node node)方法的具体实现。
private Node enq(final Node node) {
//死循环尝试,直到成功
for (;;) {
Node t = tail;
//tail不存在,设置成首节点
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//设置尾尾节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
我们可以看见上面使用了CAS来进行首节点和尾节点的设置,以达到线程安全的目的。只有尾节点设置成功了才会返回,否则会一直进行下去。过程图如下:
那么如果是出列呢?
4 出列
CLH同步队列遵循FIFO的规则,首节点的线程释放同步状态后。将会唤醒后继结点next,而后继节点将会尝试获取同步状态,如果获取同步状态成功,将会将自己设置成首节点。同时需要注意的是,这个过程后继节点会断开需前节点的关联,具体流程形如下图。