文章目录
AQS - CLH
一、CLH
-
在上一篇文章简单介绍了AQS,知道了AQS内部会维护一个同步队列,而这个队列就是CLH(Craig, Landin, and Hagersten)队列,它是一个FIFO的双向队列,AQS依赖它来完成同步状态的管理。当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
-
CLH locks ( Craig, Landin, and Hagersten, CLH锁 ) 是自旋锁,能够确保无饥饿,能够保证先来先服务的公平性。
二、Node
- Node是AbstractQueuedSynchronizer 的静态内部类。是内部队列保存的元素,它封装了一个线程的状态信息,如果线程需要
阻塞排队,那么就会封装成一个Node节点进入FIFO队列。
2.1 Node源码
static final class Node {
/**
* Marker to indicate a node is waiting in shared mode
*/
//标记当前节点为共享节点
static final Node SHARED = new Node();
/**
* Marker to indicate a node is waiting in exclusive mode
*/
//标记当前节点为独占节点
static final Node EXCLUSIVE = null;
/**
* waitStatus value to indicate thread has cancelled
*/
//表示当前节点为取消状态,因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态//不会转变为其他状态
static final int CANCELLED = 1;
/**
* waitStatus value to indicate successor's thread needs unparking
*/
//后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使
// 后继节点的线程得以运行,后继节点需要unpark
static final int SIGNAL = -1;
/**
* waitStatus value to indicate thread is waiting on condition
*/
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
/**
* Status field, taking on only the values:
* SIGNAL: The successor of this node is (or will soon be)
* blocked (via park), so the current node must
* unpark its successor when it releases or
* cancels. To avoid races, acquire methods must
* first indicate they need a signal,
* then retry the atomic acquire, and then,
* on failure, block.
* 后继节点被阻塞或者即将被阻塞,因此当前节点在释放同步状态或者取消的时候,必须unpark他的
* 后继节点,为了避免竞争,获取同步状态的方法必须先预测是否需要发信号,然后再尝试获取同步状
* 态,然后再是成功或者阻塞
* CANCELLED: This node is cancelled due to timeout or interrupt.
* Nodes never leave this state. In particular,
* a thread with cancelled node never again blocks.
* 节点因为超时或者中断取消了等待,节点会一直保持这个状态,并且不会再次阻塞
* CONDITION: This node is currently on a condition queue.
* It will not be used as a sync queue node
* until transferred, at which time the status
* will be set to 0. (Use of this value here has
* nothing to do with the other uses of the
* field, but simplifies mechanics.)
* 当前线程会在一个Condition队列,除非状态变化否则它不会进入同步队列,状态变化的时候,会被设置为0
* PROPAGATE: A releaseShared should be propagated to other
* nodes. This is set (for head node only) in
* doReleaseShared to ensure propagation
* continues, even if other operations have
* since intervened.
* 一个共享模式释放应该要无条件的传播到其他节点,这个只会被头节点在共享模式释放的时候设置,
* 来确保传播的连续
* 0: None of the above
* 不是上面的值,就是0
* <p>
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
* <p>
* The field is initialized to 0 for normal sync nodes, and
* CONDITION for condition nodes. It is modified using CAS
* (or when possible, unconditional volatile writes).
* waitStatus代表等待状态,值可能为SIGNAL/CANCELLED/CONDITION/PROPAGATE/0 这几种;
*/
volatile int waitStatus;
/**
* 前驱节点,当节点添加到同步队列时被设置(尾部添加)
*/
volatile Node prev;
/**
* 后继节点
*/
volatile Node next;
/**
* The thread that enqueued this node. Initialized on
* construction and nulled out after use.
* 封装的线程对象
*/
volatile Thread thread;
/**
* 1.如果保存的是一个特殊的值(SHARED = new Node()),就表示是共享模式
* 2.如果不是特殊值,nextWaiter表示Condition队列的下一个节点
* condition队列是独占模式,因此等待在condition的节点的队列使用一个linked queue保存节点即可
*/
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
* 判断节点是否为共享模式
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* Returns previous node, or throws NullPointerException if null.
* Use when predecessor cannot be null. The null check could
* be elided, but is present to help the VM.
*
* @return the predecessor of this node
* 返回当前节点的前驱节点,Null检查可以被省略,但是有助于VM
*/
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;
}
}
2.2 Node字段解析
- 通过表格来表示各个字段的含义:
字段 | 含义 |
---|---|
waitStatus | 等待状态,用来控制线程的阻塞和唤醒,并且可以避免不必要的#park(…)和#unpark(…) 方法。他的值为:SIGNAL/CANCELLED/CONDITION/PROPAGATE/0 ,0位初始状态。 |
prev 和 next | 分别指向当前Node的前驱和后继节点 |
head 和 tail | 分别指向当前Node链表的头结点和尾节点 |
thread | Node节点对应的线程 |
nextWaiter | Condition队列的下一个等待的线程(共享模式则是一个特殊值SHARED = new Node()) |
三、Node操作方法
3.1 addWaiter入列
3.1.1 代码
- 入队操作如果从单线程的角度来看,就是将新的Node加到队列尾部,并将原本的尾节点的后继指针指向新的Node,但是AQS中是多线程的操作,
因此在设置尾节点的时候分2个步骤,首先读取tail是node1,步骤2将尾节点从node1设置为node,步骤1和2之间很可能其他线程已经将尾节点从
node1修改为node2了,因此是不安全的,需要使用CAS设置,如果我尝试将尾节点由node1设置为node的时候,发现期望值并不是node1而已经变成
了node2,那我就再循环一次,再读取一次tail发现是node2,我再CAS设置的时候,就会成功了。(不成功就一直自旋直到成功)
/**
* 将当前线程按照指定的模式,创建一个Node对象,并加入队列
*/
private Node addWaiter(Node mode) {
//1.创建Node节点对象
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//2.快速尝试,添加新节点为尾节点
if (pred != null) {
//3.设置新 Node 节点为原尾节点(即把原尾节点作为Node的前驱)
node.prev = pred;
//4.CAS设置新的尾节点(因为是双向链表,除了将node的前驱指向原来的为节点之外,还需要将node设置为新的尾节点)
if (compareAndSetTail(pred, node)) {
//5.成功,原尾节点的下一个节点为新节点,这里就相当于把原来的为节点的后继指针指向node,到这才算一个完整的双向指针
pred.next = node;
return node;
}
}
//6.如果快速尝试失败,那就进入enq方法进行多次尝试,直到成功
enq(node);
return node;
}
/**
* 自旋设置Node为尾节点,直到成功
*/
private Node enq(final Node node) {
//自旋设置Node为尾节点,直到成功
for (; ; ) {
Node t = tail;
//1.若原尾节点为空,说明队列是空的,那就初始化,头尾节点是一样的
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//2.将新节点的前驱指向原来的队尾节点
node.prev = t;//A
//4.修改尾节点为node
if (compareAndSetTail(t, node)) { //B
//5.设置成功才会将原来的队尾的后继指针指向新的队尾节点node,
t.next = node;
return t;
}
//6.如果设置失败(比如代码A和B之间其他线程设置了尾节点,那么CAS会失败),就会进
// 入下一次循环,下一次就会重新获取Node t = tail;并设置node.prev = t;,就会
// 成功了,即使不成功,又会继续循环,其实只要AB之间没有被其他线程设置就会成功,
//CAS这样其实不会进入死循环,因为每次尝试都会获取到新的tail,然后CAS设置新的
// tail为node,但是可能会有线程自旋太久
}
}
}
- 代码中发现addWaiter方法和enq的逻辑其实类似,都是将线程封装的Node节点保存进队列,加在双向链表的队列尾部。里面用到了CAS自旋(for循环)保证线程安全,另外需要对链表有一定的了解,图片
如下。
3.1.2 入列示意图
3.2 setHead出列
- CLH 同步队列遵循 FIFO,首节点的线程释放同步状态后,将会唤醒它的下一个节点(Node.next)。而后继节点将会在获取同步状态成功时,将自己设置为首节点( head )。
这个过程相对简单,head执行该节点并断开原首节点的next和当前节点的prev即可。注意,在这个过程是不需要使用 CAS 来保证的,因为只有一个线程能够成功获取到同步
状态,并不是并发操作。
3.2.1 代码
/**
* 将node设置为队列的头结点
*/
private void setHead(Node node) {
//1.设置node成为首节点,因此head指针指向node
head = node;
//2.自己已经获得了同步状态,因此thread引用可以置为null,避免不必要的信号和遍历
node.thread = null;
//3.前驱指针也可以置null了,因为自己已经是队首
node.prev = null;
}
- 因为设置头结点的操作不是并发的,因此比较简单,就是将head指针指向对应node,并将node内部不需要的字段置为null。
3.2.2 出列示意图
四、小结
- 本文我们主要分析了AQS在对线程进行排队处理的过程中封装线程的数据结构Node,大致了解到其在Node中封装了很多状态信息,这些信息是将线程在FIFO队列中进行同步控制的关键。
- 同步过程中的关键操作是入队和出队,入队出队都是将封装后的Node节点加到FIFO队列或者从FIFO队列中移除,入队过程需要考虑线程安全,因此使用了CAS,出队过程是将一个新的节
点设置为头结点,因为只有一个线程可以修改头结点,所以不是并发操作,因此比较简单。 - 本文我们并没有涉及更上层的代码分析,这是最底层的对FIFO队列的操作,至于何时该入队何时不该入队,这些逻辑我们还没有进行分析,有了这部分的基础,后续我们在进行上层的
分析的时候,就可以不关心FIFO的队列细节,无非就是将一个Node扔进队列和取出队列的过程,AQS的代码比较复杂,我们需要一步一步的分解学习。后续的文章会进行AQS中同步状态变
量读取的相关代码解读。