AbstractQueuedSynchronizer那些事儿(五) release系列

概述

跟之前的思路一样我们也采用自顶向下的方法来分析release的具体实现

独占模式:release方法

这个方法就是独占模式下的释放锁的入口方法,可以看到非常的简单就是在释放锁成功以后,在一定条件下唤醒后继节点。
唤醒的前提是后继节点有,且后继节点进入了阻塞状态。
注意:执行唤醒的线程未必入队了,换句话说可能存在多个没有入队的线程来唤醒某一个同步队列中的节点,这是因为在acquire方法中获得锁成功后就不会入队了。

 public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

h !=null 仅仅保证了同步队列初始化了,初始化有可能还未完成,因为前面的章节中分析可知,可能head!=null,但tail==null。
h.waitStatus !=0,head节点不可能CANCELLED,其实在独占模式下这里应该也等价于h.waitStatus == SIGNAL吧,这就是说明后继节点应该被唤醒,那有可能后继节点刚刚执行完CAS来更新head状态为SIGNAL还没来得及park,当前线程就满足了条件去调用unpark,导致唤醒丢失吗?不会!如果仔细看官方的方法注释就知道unpark一个没有park的线程,这个线程的park调用不会阻塞,还是会响应唤醒操作!

这俩句组合起来的效果就是确保了同步队列初始化完成,且后继节点处于park或者即将park时,会唤醒后继节点,否则不做任何操作。那有种特殊情况,线程B即将入队,但还没有初始化前,线程A释放了锁,这里不会去唤醒线程B,接着线程B正式入队了,它会进入它的for循环尝试获取锁。

共享模式:releaseShared

共享模式下在释放共享锁成功后就直接调用doReleaseShared方法来处理了,下面再具体分析下

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

共享模式:doReleaseShared

共享模式下的释放锁方法,需要唤醒后继并确保信号传播,但是在独占模式下,只需要唤醒head节点的后继即可。

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

判断条件1.head节点不为null且不等于tail节点的情况下,这个语句保证了什么?保证了同步队列已经初始化完成了,且当前同步队列中至少有一个节点

1.判断head节点的waitStatus如果等于SIGNAL,意味着后继节点已经park或者即将park,所以需要调用unpark方法来唤醒后继节点,共享模式本就意味着可以有多个线程获取到锁,所以方法的执行有可能在多线程环境下,应该保证有多少个线程释放了锁,就唤醒多少个后继节点,从逻辑的角度来说就是先唤醒头节点的后继节点,然后出队,在唤醒一次新的头节点的后继节点...思考一下如果不用CAS,每次进来都直接调用unpark会怎样?那就可能多个线程多次唤醒了同一个head的后继节点,导致这个节点下次本应该park的都无条件的不阻塞了,破坏了同步语义!所以AQS采用了一种特别的传播机制来解决共享模式下的唤醒问题,以后在分析。现在只需要知道通过CAS来保证,至于为什么更新为0,我觉得0就是个临时状态,是为了方便CAS失败的线程进入下一个case?
2.判断head节点的waitStatus等于0且用CAS来更新waitStatus状态为PROPAGATE状态,CAS失败的线程会再次进入循环,从而读取到head节点的waitStatus是PROPAGATE了,就退出循环。为什么要continue,而不是直接break呢?代码角度来看还是为了走下面第3点的流程,这样做个人觉得是给当前线程一个机会检测到head出队从而触发新的head节点的状态更新以及唤醒操作等
3.判断当前线程看到的head此时还没有被其他线程出队,如果出队了就重复执行上述流程来触发新的唤醒操作和设置传播状态,否则就认为达成了状态一致,退出循环即可,而此时head的waitStatus有俩种,一种为0,是在刚刚尝试唤醒后继节点情况下,或者为PROPAGATE,在有线程唤醒了后继节点的情况下。

unparkSuccessor

该方法的作用就是找到入参节点的有效后继节点,并唤醒它。

//
private void unparkSuccessor(Node node) {
//
        int ws = node.waitStatus;
        //
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        //
        Node s = node.next;
        //
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

实现流程

1.CAS更新node.waitStatus=0,成功还是失败并不重要,都会走下面的流程

如果入参节点的waitStatus为负数,说明waitStatus要么为SIGNAL,要么就是PROPAGATE,先看SIGNAL,这说明它的有效后继节点已经park或者即将park了,至于用CAS来更新,我个人猜测是不是有某种可能另一个线程取消了该node,CANCELLED状态应该是最终态,所以不能简单的写waitStatus=0;

至于为何要更新这个状态,现在还没想明白。似乎不更新的话也没啥问题?

2.真正寻找有效的后继节点,并唤醒它

case1 nexd==null | next.waitStatus > 0
这个条件说明执行唤醒的线程可能看到的状态:没有后继节点,或者后继节点此时的状态变成CANCELLED,那我需要重新找一个有效的后继节点。或许有人会疑惑为什么没有后继节点还需要重新找呢?因为执行线程只是当时看到没有后继节点,但不代表后面没有新的线程入队了,所以重新再找一次。

case2 从tail节点往前遍历到head节点找到最靠近head的有效节点
为什么不从node往后遍历到tail呢?可能是因为next指针链不稳定的原因把,因为在enq方法中入队的时候是先更新tail指针,在更新next链,所以如果从后遍历,有可能读取到next==null,从而断链了,找不到新插入的tail节点,而向前遍历不会,读取到最新的tail节点,其prev一定有值,所以一定可以找到有效节点。

你可能感兴趣的:(java,并发)