1.头结点与获取锁
头结点是获取锁的关键,对于Exclusive模式来说
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
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);
}
}
当前节点获取锁的条件有两个:
1.当前节点的前驱是头节点
2.当前节点成功获取锁
其实如果是
非公平锁,那么在该代码片显示的逻辑之外还有一个
fast path,即在老二位置的节点(这里把前驱是头结点的节点称为老二节点)能不能获取锁,要看通过fastpath进行tryAcquire的节点是否能够,详见 ReentrantLock(重入锁)以及公平性,在后面说公平性锁的时候再详细说。
2.头结点与setHead
/**
* Sets head of queue to be node, thus dequeuing. Called only by
* acquire methods. Also nulls out unused fields for sake of GC
* and to suppress unnecessary signals and traversals.
*
* @param node the node
*/
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
根据注释得知,该方法只在acquire方法中调用,我们已知的有acquireQueued,doAcquireInterruptibly和doAcquireNanos这三个自旋获得锁的方法中调用。
对于share模式,doAcquireShared,doAcquireSharedInterruptibly和doAcquireSharedNanos,调用是其变体setHeadAndPropagate方法,下一个段落说这个方法。
setHead方法把当前节点设置为头结点,同时将thread变量和prev变量置空(help gc)。
setHead方法只在acquire方法中成功获得锁的情况下调用,所以不存在racing,所以没有采用原子操作,不想set tail的操作。
3.头结点声明周期——入队列(当前线程无法获取锁)初始化
/**
* 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) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
如果当前线程无法获取锁,会用当前线程构造一个节点同时入队列。这里也有一个fast path(先尝试原子设置队尾,不考虑整个队列是否有初始化过,是否有头结点),如果失败,执行入队列操作。
/**
* 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 (;;) {
Node t = 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;
}
}
}
}
如果发现tail==null(说明head也是null),说明队列没有初始化,这里要初始化,原子设置空的头节点(compareAndSetHead),并把tail也指向该节点。然后把当前节点原子设置到tail上(compareAndSetTail)。
4.头结点设置状态——前驱是头结点,当前节点tryAcquire失败时
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);
}
}
现在假设只有两个线程,当第一个线程T1获得锁并占用锁,第二个线程T2来访问该锁,有两种情况:
1.第一种是T2在acquireQueued中进行tryAcquire成功
这说明该线程在tryAcquire时,第一个线程T1已经释放了锁。
这时不会走shouldParkAfterFailedAcquire,即不会设置状态,因为该节点已成功获取锁,不需要T1去唤醒T2。
T1在release的时候,头节点的状态是未初始化的,所以不会调用unparkSuccessor方法。
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
2.第二种是T2在acquireQueued中进行tryAcquire失败
这时会调用shouldParkAfterFailedAcquire初始化头节点的状态为SIGNAL,说明需要T1在release时唤醒T2。
T1在release的时候,判断头节点的状态是已经初始化过的,所以会调用unparkSuccessor方法。
在unparkSuccessor方法中会剔除头节点的状态。
5.头尾节点重逢
继续刚才的情境,T2在T2进行unparkSuccessor成功之后,acquireQueued中继续运行,这时T2的前驱节点是head,同时tryAcquire可以成功。
把T2对应的节点用setHead设置为头结点,清空pred和thread字段,而该节点同时也是tail节点,这样head和tail都是同一个节点了,回到了初始化状态。
这种情况有可能是在"非公平锁"的情况下因为有fast path出现的现象。这时也会进行park操作。
6.总结
对于队列中的节点获取锁释放锁的过程可以描述为(这里假设只有三个线程竞争资源):
1.初始状态,sync队列没有初始化,当前线程无法获取锁,进入队列,设置一个空节点作为头结点,当前节点作为头结点的后继——老二节点,并把头结点状态设置为SIGNAL。
2.又来一个线程来获取锁,没有获取到,进入队列,成为老三节点(老二节点的后继),并设置老二节点状态为SINGAL,但是这个时候shouldParkAfterFailedAcquire返回false,即
不进行park操作,再给一次判断head并且tryAcquire的机会。
3.队列外的线程释放了锁,并判断当前头结点状态为SIGNAL,unparkSuccessor释放其后继节点——老二节点。
4.老二节点获取到锁,并把自己设置为头节点,老三节点成为了老二节点,然后执行相关业务操作。
5.头结点(原来的老二节点)执行完业务操作后释放锁(
调用unlock),判断头结点(自己)状态为SIGNAL,释放其后继节点,即老二节点(原来的老三节点)。
6.老二节点(原先老三节点)获取锁,把自己设置为头节点,并执行业务操作。
7.头结点(原先的老三节点,后来的老二节点)释放锁,发现自己的状态是0,不再执行唤醒操作。
7.节点状态的意义
对于公平锁来说,没有竞争的话就没有节点。有竞争的话就有节点入队列,并且之后每次都会让队列中的节点先拿到锁。
对于非公平锁来说,没有竞争就没有节点。有竞争的话就有节点如队列,但是之后不一定保证队列中的节点先拿到锁,有可能新来的线程会先抢到锁。
节点的状态表示了其后继节点是否要获得锁。如果节点状态是0(没有初始化),说明没有后继节点。反之,有后继节点,并且后继节点要获取锁。