AbstractQueuedSynchronizer(八)——头节点和尾节点

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(没有初始化),说明没有后继节点。反之,有后继节点,并且后继节点要获取锁。

你可能感兴趣的:(java)