JUC -AbstractQueuedSynchronizer 源码:从独占与共享的角度分析(待续)

关于AbstractQueuedSynchronizer这个类作用的理解

AbstractQueuedSynchronizer(AQS)并没有限制只能用于实现锁,它是一个资源同步器,提供的方法仅实现了在尝试获取资源失败后,将线程阻塞并放入一个队列(同步队列syncQueue)中,等到合适的时机再次唤醒线程并尝试获取资源。
AbstractQueuedSynchronizer并不实现如何获取资源,这交由子类来实现。
在AQS中,资源指的是一个int类型的变量state同步队列是一个双向链表,链表的每一个节点封装了等待申请资源的线程。获取资源的模式有两种:独占模式和共享模式。
独占模式是指在同一时刻,只可能有一个线程能够访问资源(state),根据这个定义可以很容易想象到应用场景:独占锁。
共享模式则是同一时刻可能有多个线程能够访问资源。
两种模式下,线程申请资源失败后,都将进入到同步队列中等待被唤醒,不同的是,线程被唤醒时调度的思路。

独占模式的调度思路

独占模式获取资源失败后阻塞进入同步队列,在资源被释放时,只有同步队列头部的一个节点会被唤醒,唤醒后将尝试获取资源。
若获取资源成功,该线程从同步队列中出队。这保证了资源被释放后,将只有一个线程竞争资源,较大了提高了竞争成功率,降低了唤醒多个线程导致的线程调度开销。
若获取资源失败(说明有其他线程竞争到了资源),该线程将保留在同步队列头部,并继续阻塞,等待下一次被唤醒。

共享模式的调度思路

共享模式获取资源失败后同样阻塞并进入同步队列,但是在资源被释放时,有可能会唤醒多个线程去获取资源。资源被释放时,首先头部的一个节点会被唤醒,然后该节点将尝试获取资源。
如果获取资源成功,则会判断是否还有可用的资源,如果有,且被唤醒节点的后继节点是共享模式,则会唤醒后继节点;如果没有可用资源,或后继节点不是共享模式节点,则不会继续唤醒。
如果获取资源失败,该线程保留在同步队列头部,等待下一次被唤醒。
这里可以看出,如果同步队列中的节点是这样子分布的,且节点A被唤醒获取到资源后,还有可用的资源,节点B将会被唤醒。而节点B申请资源成功后,无论是否还有可用资源,节点C都不会被唤醒。
在这里插入图片描述

AbstractQueuedSynchronizer主要方法

// 新节点插入同步队列尾部,先尝试一次插入尾部,若CAS失败则调用enq方法入队
private Node addWaiter(Node mode);
// 也是新节点插入同步队列尾部,但是在CAS操作失败时将循环直到成功
private Node enq(final Node node);

// 以独占模式申请资源
public final void acquire(int arg);
// 以独占模式申请资源,并在线程被中断时抛出异常
public final void acquireInterruptibly(int arg);
// 以独占模式申请资源,指定时间内没有成功获取到,则抛出异常
public final boolean tryAcquireNanos(int arg, long nanosTimeout);
// 尝试以独占模式申请资源,这个方法由子类实现
protected boolean tryAcquire(int arg);
// 尝试获取资源,获取失败时阻塞线程
final boolean acquireQueued(final Node node, int arg);
// 取消获取资源,并从同步队列中移除节点
private void cancelAcquire(Node node);

// 释放独占模式的资源,这个方法会唤醒同步队列中的第一个节点
public final boolean release(int arg);
// 尝试释放独占资源,这个方法由子类实现
protected boolean tryRelease(int arg);
// 唤醒入参节点的后继节点
private void unparkSuccessor(Node node);

// 以共享模式申请资源
public final void acquireShared(int arg);
// 以共享模式申请资源,线程被中断时抛出异常
public final void acquireSharedInterruptibly(int arg);
// 以共享模式申请资源,指定时间内没有成功获取到,则抛出异常
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout);
// 尝试获取资源,获取失败时阻塞,成功时判断是否要唤醒后继节点
private void doAcquireShared(int arg);

// 释放共享资源,并唤醒头部的共享节点
public final boolean releaseShared(int arg);
// 尝试释放共享资源,这个方法由子类实现
protected boolean tryReleaseShared(int arg);
// 唤醒头部的共享节点
private void doReleaseShared();

// 判断当前线程是否独占资源
protected boolean isHeldExclusively();

最重要的用于实现如何申请与释放资源的四个方法,tryAcquiretryReleasetryAcquireSharedtryReleaseShared均交由子类实现,这给了子类很大自由决定如何使用state这个变量。

同步队列 syncQueue

同步队列是维护在AQS内的一个双向链表,AQS的headtail引用指向链表的头尾节点。其中head节点是一个空节点,其中不封装任务线程,但是其waitStatus状态被用于判断“唤醒”动作是否向后继节点传播。
同步队列节点的定义如下:

static final class Node {
    /** 表示当前节点已经取消竞争状态,可以从syncQueue中移除 */
    static final int CANCELLED =  1;
    
    /** 表示当前节点的后继节点希望当前节点能够唤醒后继节点,一个节点的SIGNAL状态是由后继节点包含的线程为其设置的 */
    static final int SIGNAL    = -1;
    /** 表示当前节点在waitQueue中等待获取资源,只有在等待队列中才存在该状态 */
    static final int CONDITION = -2;
    /** 
     * 只有head节点可能是这个状态,且是由拥有资源的节点为其设置的
     * 表示下一个拥有资源的节点,应当传播“唤醒”动作,在某些时刻需要(刚获取到资源、释放资源)考虑是否唤醒其后继节点 
     */
    static final int PROPAGATE = -3;

    /**
     * 节点的状态,在同步队列syncQueue中,Node的waitStatus取值只可能有CANCELLED,SIGNAL,PROPAGATE三种,也就是 1 -1 -3
     * 在等待队列conditionQueue中,Node的waitStatus取值只可能是CANCELLED,CONDITION
     */
    volatile int waitStatus;

    /**
     * 节点代表的线程
     */
    volatile Thread thread;

    /**
     * 在等待队列waitQueue中,用来指向下一个节点;
     * 在同步队列syncQueue中,用来表示节点像要以独占(EXCLUSIVE)或共享(SHARE)的方式获取到资源
     */
    Node nextWaiter;
}

独占模式资源的申请与释放

首先忽略掉tryAcquiretryRelease这两个方法的实现,忽略掉子类如何实现申请资源与释放资源,仅关注在申请和释放资源释放成功上,从申请资源到释放资源的流程,来看一下AQS的实现。

申请独占资源

独占资源申请的方法调用如图:
在这里插入图片描述
源码分析:

/**
 * 以独占模式申请资源,参数arg被传递给tryAcquire,对于acquire方法来说无意义,
 * 具体arg有什么效果在子类实现的tryAcquire中定义
 * 如果线程被中断,也会获取到资源,并将线程的状态设置为INTERRUPTED,然后返回
 */
public final void acquire(int arg) {
	// 首先尝试获取一次资源,如果成功直接返回
	// 如果未成功获取到资源,将当前线程包装为独占模式节点,并进入到syncQueue竞争资源
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 如果获取到资源的时候,线程被中断了,这里再调用一下thread.interrupt()方法中断当前线程,让外面的调用者获知有一次中断
        // 疑问:被interrupt中断唤醒的park,并不会清楚中断状态,这里为什么还要再中断一次?
        selfInterrupt();
}

/**
 * 添加一个节点到同步队列尾部,并返回这个新增的节点
 */
private Node addWaiter(Node mode) {
	// 将当前线程包装为Node,由参数指定模式
    Node node = new Node(Thread.currentThread(), mode);
    
    // 这一部分代码是不是和enq入队代码很像?没错,这里就是先尝试一次入队操作,如果成功直接就返回了
    // 如果不成功就调用enq方法,循环的插入到队列尾部
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

/**
 * 将节点插入到syncQueue的尾部
 */
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { 
        	// 初始状态下,syncQueue的头尾指针都是空的,在这里第一次节点入队的时候进行初始化
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
        	// 入队到尾部
            node.prev = t;
            // 这里CAS设置尾节点完成后,才将原来尾节点的next指向现在的尾节点
            // 所以在某一瞬间可能出现 从head遍历访问next访问不到tail 的情况
            // 可能这就是后面遍历才用从tail访问prev到head的原因
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

/**
 * 已经在syncQueue队列中的线程通过此方法尝试获取资源,成功获取到资源后返回此线程是否被中断;
 */
final boolean acquireQueued(final Node node, int arg) {
	// 记录是否有异常
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 死循环,不断尝试获取资源,成功才退出,不成功就一直循环
        for (;;) {
        	// 当前节点的前驱节点如果是head,则尝试获取资源,成功则返回
        	// 这里可以看出来,syncQueue中等待的线程是按FIFO的模式去获取独占资源的,先到先得,公平
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
            	// 当前线程获取到独占资源后,将当前节点设置为头结点,清空thread和前驱指针,相当于获取到独占资源后将节点出队
                setHead(node);
                p.next = null; // help GC  出队节点的next置空,但是就算不置空,Parallel GC的标记整理算法也会回收的,又不是用的引用计数,不明白为什么能 help GC
                failed = false;
                return interrupted;
            }
            // 尝试获取资源失败,判断是直接开始下一次尝试,还是先暂停当前线程的调度
            // 如果暂停了当前线程调度,当线程被唤醒之后,还需要检测一下线程是否被中断唤醒
            //    因为LockSupport.lock方法暂停调度后,一般有两种情况会被唤醒:
            //	     1. 其他线程调用LockSupport.unpark唤醒;
            //       2. interrupt中断唤醒;
            //    需要判断是否被interrupt唤醒,如果是,要来由调用者决定是否抛出异常,所以需要返回interrupted状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
    	// 如果是发生异常退出方法,需要取消申请获取资源
    	// 这里并不是直接将节点从syncQueue中移除,而是先将节点设置为CANCELLED状态,
    	// 然后在从head到tail遍历的next指针上将CANCELLED节点移除
        if (failed)
            cancelAcquire(node);
    }
}

/**
 * 取消申请资源,并在next指针方向上移除CANCELLED的节点
 * 为了便于分析情况,假设在进入cancelAcquire之前同步队列状态如图中第1步所示
 */
private void cancelAcquire(Node node) {
    // 节点为空直接返回
    // 疑问:什么时候会为空?
    if (node == null)
        return;

    // 释放线程引用,帮助GC
    node.thread = null;

    // 将该节点以及n个前驱的CANCELLED状态的节点都移除掉,并找到前面第一个非CANCELLED状态的节点作为当前后继节点的前驱
    // 这里只更新了当前节点的prev指针,在tail->prev->head方向上把CANCELLED节点移除了
    // 此处参考图中的第2步
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // 拿到这个引用是为了做CAS
    Node predNext = pred.next;

    // 当前节点设置为CANCELLED,此时同步队列参考图中第2步
    node.waitStatus = Node.CANCELLED;

    // 如果当前节点是tail,将前驱节点设置为tail,这样相当于把node和前面的CANCELLED节点删掉了
    if (node == tail && compareAndSetTail(node, pred)) {
    	// 设置完tail,CAS设置下前驱节点的next,失败也没关系,为什么没关系?
    	// 因为失败说明tail的next被另一个线程修改了,什么时候会修改tail的next呢?
    	// 是插入新的节点到syncQueue的时候,这个时候tail的next已经不应该被设置为null了,所以这里失败也没关系
        compareAndSetNext(pred, predNext, null);
        // 这一步执行完,同步队列状态类似图中第3步
    } else {// 如果CAS设置tail失败,说明tail变成的别的节点,即此时有新的节点入队,node不再是tail
        // 当前节点不是tail,如果前驱节点也不是head,要保证前驱节点状态为SIGNAL,能够唤醒下一个节点
        // 如果前驱不是head且保证状态是SIGNAL,且前驱节点线程存在(不是空节点,变相判断不是head),尝试修改前驱节点的next
        //     这样就实现了从syncQueue中移除了n个CANCELLED状态的节点和当前节点
        //     虽然node.next.prev指针没有修改,但是唤醒是通过next指针来的,next修改后,CANCELLED的节点就不会被唤醒了
        //     只是会出现 从head向tail遍历 和 从tail向head遍历 得到的节点不同,从tail到head会多几个CANCELLED状态的节点,
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
                // 如果这一步执行成功,同步队列状态类似图中第5步
        } else {
        	// 前驱节点是head,或者无法将前驱节点设置为SIGNAL(前驱变成了CANCELLED),则直接唤醒下一个节点参与竞争资源
            unparkSuccessor(node);
            // 唤醒后,同步队列状态类似图中第4步
            // 第4步这里双向链表结构其实已经被破坏了(E节点的next指向自身而不是tail),
            // 但是线程F所在节点被唤醒,此时F不是head的后继,必然不会去申请资源
            // 会调用到shouldParkAfterFailedAcquire方法,在这个方法中会对F的前驱节点进行整理,恢复双向链表
        }

        node.next = node; // help GC
    }
}

在这里插入图片描述


/**
 * 检测节点在尝试获取资源失败之后,是否应该park暂停调度
 * 这里保证了只有在前驱节点状态为SIGNAL的时候,当前线程才会阻塞
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 如果前驱节点已经是SIGNAL状态,则阻塞暂停调度
    if (ws == Node.SIGNAL)
        return true;

    // ws > 0 说明前驱节点是CANCELLED状态,这时候把CANCELLED状态的前驱节点从syncQueue中移除掉
    // 这里对应的就是cancelAcquire中的第4步情况,恢复同步列表的双向链表结构,恢复完成后的图比较简单就不画了
    // 移除完CANCELLED节点后并没有保证前驱为SIGNAL,所以返回false不阻塞
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
    	// ws <= 0 说明是0或者PROPAGATE(SIGNAL已经在前面判断过了)
    	// 需要把前驱节点设置为SIGNAL,让前驱节点知道在释放资源时唤醒当前线程,这样才能让当前线程阻塞
    	//    如果不设置就直接阻塞的话,没人来唤醒当前线程,不就死掉了
    	// 这里没有自旋判断是否设置SIGNAL成功,而是返回false先继续尝试获取资源
    	// 因为都是自旋,在这里自旋设置状态不如自旋尝试获取资源,拿到资源的几率还上升了
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

另外两个申请独占资源的方法acquireInterruptiblytryAcquireNanosacquire方法大同小异,区别仅是在不同时机会抛出中断异常,此处不再分析。

释放独占资源

释放独占资源的流程较为简单,尝试释放资源,成功就唤醒后继节点并返回true,失败则返回false。
在这里插入图片描述
源码分析:

/**
 * 释放独占模式的资源,并唤醒syncQueue中的一个线程
 */
public final boolean release(int arg) {
	// 首先尝试释放资源,是否能成功释放交由子类实现
    if (tryRelease(arg)) {
    	// 成功释放资源后,如果头结点状态不等于0,则头结点肯定是SIGNAL或PROPAGATE,需要唤醒后继节点
    	// 这里头结点状态不可能为CANCELLED,分析一下:
    	//    头结点有两种可能,第一种是初始化的空节点,而空节点是不可能被cancel的;
    	//    第二种可能是头结点是上一个获取到独占资源的线程节点,而获取到资源的节点必然不可能是CANCELLED状态的(CANCELLED状态的节点不可能被唤醒参与资源竞争)
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

/**
 * 唤醒传入节点的后继节点
 */
private void unparkSuccessor(Node node) {
    /*
     * 如果waitStatus为负数,也就是SIGNAL或PROPAGATE,说明后继节点可能会等着被唤醒,
     * 而这个方法的目的就是唤醒后继节点,所以需要清除这个等待被唤醒的状态
     * 失败也没关系,当前节点唤醒后继之后就会释放资源,从syncQueue头部移出
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * 如果后继为节点null,或者后继节点被取消(CANCELLED状态, waitStatus > 0)
     * 从尾节点向前遍历,找到当前节点之后第一个等待唤醒的后继节点
     *
     * 这里为什么后继节点可能为null?
     *    当前节点是尾节点时,后面没有线程在等待了,后继为null;
     * 为什么要从尾节点向前遍历? 
     *    如果后继节点为null,后面没有了,当然从尾节点向前遍历;
     *    如果后继节点是CANCELLED,参考前面分析cancelAcquire方法可能导致同步队列出现的情况,其中一种情况会破坏同步队列的双向链表结构
     *        即使被破坏了,从tail向head方向的单向链表是保留好的,因此这里选择从tail向head遍历
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从tail向前,找到node之后第一个状态是SIGNAL或PROPAGATE的节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 如果这样的后继节点存在,则唤醒后继节点中的线程
    if (s != null)
        LockSupport.unpark(s.thread);
}

独占模式的应用

根据独占模式的特征只有一个线程能够拥有资源,很容易想到一个应用场景:互斥锁。JUC包中的互斥锁实现,最常用的是ReentrantLockReentrantReadWriteLock也是互斥锁,其中的读锁和写锁、写锁和写锁互斥。StampedLock虽然也是读写互斥锁,但并不是基于AQS实现的。

  1. ReentrantLock源码分析 TODO
  2. ReentrantReadWriteLock源码分析 TODO

共享模式资源的申请与释放

在分析共享模式资源申请与释放时,同样忽略掉tryAcquireSharedtryReleaseShared方法的实现;需要关注的是tryAcquireShared方法返回值的意义,其方法签名为:

/** 
 * 返回值 < 0  申请资源失败
 * 返回值 == 0 申请资源成功,但没有多余可用的资源给后继阻塞的线程获取
 * 返回值 > 0  申请资源成功,且共享资源可以让后继线程继续获取
 */
protected int tryAcquireShared(int arg)

入参arg在分析AQS代码的时候不关心其实际意义,它仅是acquireShared方法传递过来的参数而已。返回值是一个int,它代表着是否还有剩余的共享资源可以申请,当还有共享资源可申请时,且获取到共享资源的节点状态为PROPAGATE,需要唤醒下一个阻塞的共享模式节点,让它去继续申请共享资源,这就是PROPAGATE这个状态“传播”的含义。

申请共享资源

申请共享资源方法调用流程图:
在这里插入图片描述
源码分析(有些方法在前面分析过了,比如shouldParkAfterFailedAcquire这种,就不再重复分析):

/**
 * 共享模式获取资源,如果线程被中断,继续获取资源,并在成功时设置当前线程状态为INTERRUPTED
 */
public final void acquireShared(int arg) {
	// 尝试一次获取,成功直接返回,即使还有共享资源也不传播唤醒动作,在释放时再唤醒后继
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

/**
 * 以共享模式申请资源,不会抛出中断异常
 */
private void doAcquireShared(int arg) {
	// 添加一个共享模式节点到syncQueue
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
        	// 虽然不是原来的味道,但确实是原来的配方,前驱为head则尝试获取资源,FIFO
            final Node p = node.predecessor();
            if (p == head) {
            	// 尝试申请资源,返回剩余可用资源数量
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                	// 申请到了资源,当前节点从头部出队,并检测是否需要传播唤醒动作
                    setHeadAndPropagate(node, r);

                    // head被设置成了当前节点,之前的head可以从syncQueue中移除了,next设置为空
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 申请资源失败是否应该阻塞?(shouldParkAfterFailedAcquire)
            //    前一个节点是SIGNAL则阻塞;
            //    前一个节点是CANCELLED则移除掉前面的CANCELLED节点,且不阻塞;
            //    前一个节点是0或PROPAGATE,尝试则将其设置为SIGNAL,且不阻塞;
            // 阻塞唤醒之后继续尝试,但如果线程被中断则记录一下
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

/**
 * 设置当前节点为头结点,并在资源足够或后继节点需要被唤醒时传播唤醒,
 * 设置为头结点后,当前节点出队,但保留waitStatus在头结点中;
 * 传播唤醒,是怎么一个操作呢?
 *      如果头结点状态是SIGNAL,则唤醒后继节点;唤醒后后继节点会进行获取资源的尝试;当前线程被唤醒后获取到了资源,并且还唤醒了下一个线程来获取资源,这就实现了传播;
 *      如果头结点是0,则设置为PROPAGATE;头结点为0,说明没有后继节点参与资源竞争或后继节点还没来得及将头结点设置为SIGNAL(shouldParkAfterFailedAcquire中还没执行到)
 *         此时将头结点设置为PROPAGATE,则后继节点在shouldParkAfterFailedAcquire中会再将头结点设置为SIGNAL,并且继续尝试获取资源而不是park,
 *         从而阻止了一次后继节点的park,也算是将唤醒向后传播了一个节点(在park之后唤醒和阻止一次park效果一样,还节省了线程调度的开销)
 */
private void setHeadAndPropagate(Node node, int propagate) {
	// 设置当前节点为头结点,也就是把当前节点置空,因为当前线程走到这里说明已经获取到资源了
	// 这里为啥要先存一下之前的head?
	//     在设置新head前后,如果新旧head有一个的状态是SIGNAL或PROPAGATE(都表示要唤醒后继节点),都需要唤醒下一个
    Node h = head; // Record old head for check below
    setHead(node);

    // 如果剩余可用资源大于0,
    // 或者之前的head为null,或之前head状态为SIGNAL或PROPAGATE,
    // 或者现在的head为null,或现在head状态为SIGNAL或PROPAGATE,
    // 疑问:为什么head会为null,如果是同步队列未初始化,那也不会入队等待
    // 
    // 就是说,如果还有剩余可用的资源,而且前一个head或现在的head被设置为需要唤醒下一个节点,就唤醒下一个共享模式的节点。
    // 道格大神的注释写道,这么多”短路或”检测可能会造成不必要的多次唤醒,但只有在多线程竞争共享资源时才会发生,多数情况是需要立即唤醒的。
    // 另外,这里检测waitStatus用了正负判断而不是直接判断等于PROPAGATE,是因为PROPAGATE可能转换为SIGNAL
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
    	// 拿到当前节点的后继节点,如果存在且是共享模式,就释放共享资源
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

/**
 * 这个方法并不是释放资源,而是释放资源之后的处理:
 * 唤醒head节点的后继节点,或者将head节点状态设置为PROPAGATE;
 * 线程退出此方法时,要么唤醒了head的后继节点,要么将head节点设置为了PROPAGATE
 *   如果是唤醒了后继节点,则后继节点申请资源成功时会再进入这里;申请资源失败时会进入shouldParkAfterFailedAcquire,会将head状态变为SIGNAL
 *   如果是将head节点设置为了PROPAGATE,则下一次申请资源的共享节点线程也会将PROPAGATE设置为SIGNAL,并且进行一次自旋;
 * 
 * 所以,这个把head设置为PROPAGATE的操作,除了让后继节点在申请资源失败时多了一次自旋之外,还有什么意义呢?
 * 
 */
private void doReleaseShared() {
    // 这里为什么要循环?根据代码内容推断,是防止竞争失败;
    // 这里判断了head,head指针发生变化时重新进入循环,什么时候head会变化?头结点的后继节点获取到资源的时候;
    // 这里什么时候会调用?头结点的后继节点获取到资源的时候(acquireShared成功)、拥有资源的线程释放资源的时候(releaseShared成功);
    for (;;) {
        Node h = head;
        // head不为null,且不等于tail,说明syncQueue不为空
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 如果head状态SIGNAL,在释放时需要唤醒下一个,尝试将SIGNAL清空;成功时唤醒下一个节点时退出;失败时重新循环;
            if (ws == Node.SIGNAL) {
            	// acquireShared成功和releaseShared成功时,都会进入这个方法,这里有可能会发生竞争
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 这里在唤醒head后继的时候,没有判断节点类型,有可能唤醒的是一个独占模式节点
                unparkSuccessor(h);
            }
            // 如果head状态为0,说明syncQueue中没有要唤醒的节点或还没来得及将头结点设置为SIGNAL
            // 这个时候把头结点状态设置值为PROPAGATE
            // 设置为PROPAGATE之后,后继节点申请资源时如果失败,在shouldParkAfterFailedAcquire方法中,会判断到头结点状态 <= 0
            //      不会再park,直接将其设置为SIGNAL,然后尝试下一轮获取资源,这样就防止了一次park
            // 而在shouldParkAfterFailedAcquire中将前驱节点设置为SIGNAL时的判断条件是 ws <= 0,那是不是意味着这里不把0改为PROPAGATE也是ok的?
            // 如果上述成立,那这个PROPAGATE在syncQueue的竞争中其实和0值作用一样,其意义是什么呢?就是为了区分0值吗?
            // CAS同样是因为存在竞争;
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 这次循环期间syncQueue的head没有发生过变化,就跳出了,达到了唤醒第一个等待节点或设置head为PROPAGATE的目的
        // 如果发生了变化,说明第一个在等待的节点发生了变化,要重新尝试唤醒下新的第一个等待的共享模式节点
        if (h == head)                   // loop if head changed
            break;
    }
}

释放共享资源

共享模式的应用

条件等待

独占与共享模式同时使用的场景

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值