根据目前对队列同步器的理解,先把目前对AQS-队列同步器认识记录一下。然后根据源码的注释,一点点进行解析
首先说明一下CLH队列
- 下面是AQS中对Node节点的注释,然后看看CLH队列
- CLH(Craig, Landin, andHagersten)根据三个人的名字命名的队列
- 它是队列锁的一个变种(variant),通常用于自旋锁(这个源码中会有体现),用它代替阻塞同步器
- 在节点中用"status"字段来跟踪每个节点是否处于阻塞状态
- 每个节点在在其前面一个节点释放同步状态后,前驱节点将会通知该节点去获取同步状态--即:发出信号通知该节点
- CLH队列中的每个节点中的线程都有机会尝试获取同步状态,但是并不能保证一定获取成功
- CLH是一个双向链表队列,(就数据结构而言,算是比较简单的数据结构了,相对其他的数据结构来说)
- 加入到CLH队列中的节点会被加入到队列的尾部,并保证加入时的原子性
- 节点弹出队列的时候只需要将头节点head移除即可
- 节点在确定它的前驱节点时,需要耗费一定的操作,因为要处理部分节点被取消或者中断
- 队列中的每个节点都有一个唯一的线程,用该线程来获取同步状态
* Wait queue node class.
*
* The wait queue is a variant of a "CLH" (Craig, Landin, and
* Hagersten) lock queue. CLH locks are normally used for
* spinlocks. We instead use them for blocking synchronizers, but
* use the same basic tactic of holding some of the control
* information about a thread in the predecessor of its node. A
* "status" field in each node keeps track of whether a thread
* should block. A node is signalled when its predecessor
* releases. Each node of the queue otherwise serves as a
* specific-notification-style monitor holding a single waiting
* thread. The status field does NOT control whether threads are
* granted locks etc though. A thread may try to acquire if it is
* first in the queue. But being first does not guarantee success;
* it only gives the right to contend. So the currently released
* contender thread may need to rewait.
*
*
To enqueue into a CLH lock, you atomically splice it in as new
* tail. To dequeue, you just set the head field.
*
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
*
*
* Insertion into a CLH queue requires only a single atomic
* operation on "tail", so there is a simple atomic point of
* demarcation from unqueued to queued. Similarly, dequeuing
* involves only updating the "head". However, it takes a bit
* more work for nodes to determine who their successors are,
* in part to deal with possible cancellation due to timeouts
* and interrupts.
*
*
The "prev" links (not used in original CLH locks), are mainly
* needed to handle cancellation. If a node is cancelled, its
* successor is (normally) relinked to a non-cancelled
* predecessor. For explanation of similar mechanics in the case
* of spin locks, see the papers by Scott and Scherer at
* http://www.cs.rochester.edu/u/scott/synchronization/
*
*
We also use "next" links to implement blocking mechanics.
* The thread id for each node is kept in its own node, so a
* predecessor signals the next node to wake up by traversing
* next link to determine which thread it is. Determination of
* successor must avoid races with newly queued nodes to set
* the "next" fields of their predecessors. This is solved
* when necessary by checking backwards from the atomically
* updated "tail" when a node's successor appears to be null.
* (Or, said differently, the next-links are an optimization
* so that we don't usually need a backward scan.)
*
*
Cancellation introduces some conservatism to the basic
* algorithms. Since we must poll for cancellation of other
* nodes, we can miss noticing whether a cancelled node is
* ahead or behind us. This is dealt with by always unparking
* successors upon cancellation, allowing them to stabilize on
* a new predecessor, unless we can identify an uncancelled
* predecessor who will carry this responsibility.
*
*
CLH queues need a dummy header node to get started. But
* we don't create them on construction, because it would be wasted
* effort if there is never contention. Instead, the node
* is constructed and head and tail pointers are set upon first
* contention.
*
*
Threads waiting on Conditions use the same nodes, but
* use an additional link. Conditions only need to link nodes
* in simple (non-concurrent) linked queues because they are
* only accessed when exclusively held. Upon await, a node is
* inserted into a condition queue. Upon signal, the node is
* transferred to the main queue. A special value of status
* field is used to mark which queue a node is on.
*
*
Thanks go to Dave Dice, Mark Moir, Victor Luchangco, Bill
* Scherer and Michael Scott, along with members of JSR-166
* expert group, for helpful ideas, discussions, and critiques
* on the design of this class.
*/
CLH队列示意图(画的很粗糙)
队列入队API
/**
* Creates and enqueues node for current thread and given mode.
* 新节点的创建并入队在制定模式下(共享式和独占式)
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
//1、为当前线程创建新节点
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速插入,如果失败则用enq插入
//1、获取同步器中的tail指向的节点,即:未插入新节点时的尾节点(暂且称之为原队列中的尾节点)
Node pred = tail;
if (pred != null) {//判断尾节点不为空,为空则使用enq插入,enq中会存在创建head和Tail节点的逻辑
//2、新节点的前驱节点指向原尾节点
node.prev = pred;
//3、使用CAS设置尾节点(AQS代码风格之一:将操作放入if判断中)
if (compareAndSetTail(pred, node)) {
//4、将原尾节点的next指向新节点
pred.next = node;
return node;
}
}
//如果快速插入队列失败,则用enq进行插入
enq(node);
return node;
}
/**
* Inserts node into queue, initializing if necessary. See picture above.
* 将节点插入队列,如果有必要(未节点为空),即:队列为空,则初始化队列
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
//类似节点获取同步状态时的自旋,其实就是有返回条件的死循环
for (;;) {
//1、将同步器中的未节点赋给临时变量t
Node t = tail;
if (t == null) { // Must initialize
//2、如果未节点为空,就是队列为空,则说明队列为空,新建一个节点,使用CAS设置头结点。,CAS保证操作的原子性
if (compareAndSetHead(new Node()))
//3、如果头结点设置成功,则将同步器中的未节点指向头结点,然后继续步骤1
tail = head;
} else {
//4、将新节点的前驱节点指向原队列中的未节点
node.prev = t;
//5、使用CAS设置未节点,即:同步器中的tail指向新节点
if (compareAndSetTail(t, node)) {
//6、如果未节点设置成功,则将原未节点的next指向新节点
t.next = node;
return t;
}
}
}
}
---
1、不知道随着AQS的深入研究,回过头来看看现在对源码的理解,是否会笑
2、时刻记录自己的想法,就算错了,以后也知道我为什么错了,我哪里错了,我为什么会错
3、我只是不想那么轻易的认输