AbstractQueuedSynchronizer 原理 & 源码

闲话

看ThreadPoolExecutor 源码的时候,其中 Worker 类是基于 AbstractQueuedSynchronizer 构建的,所以顺便把这个类一起看了。另外,这个类也是ReenTrantLock 和 Semaphore 的底层的机制,说明足够重要了。为了方便,下文统一使用AQS 指代 AbstractQueuedSynchronizer 。

一、简单了解

1.1、AQS —为了同步而生

AQS 由 Doug Lea 大神编写,主要目的是提供同步功能。AQS 内部维护了一个 int 类型的变量 state。这个 state 的不同值,可以代表不同的状态,AQS 操作 state 变量时,使用 CAS,保证了原子性(多线程环境下安全)。通过在多线程环境下,对 state 不同的值代表的不同状态进行赋予不同的含义,AQS 就可以当做一个比较完备的同步器来使用。这里,对”状态“这个词做下解释,举两个例子。第一个例子是互斥锁:state 为0,代表互斥锁被占用,state 为1,代表互斥锁可用。第二个例子是信号量(对共享资源的访问):state 为10,代表当前有10个资源(例如数据库连接)可用,state 为5,代表当前有 5个资源(数据库连接)可用,state <= 0,则代表当前没有资源可用,必须等待其他资源释放后,才能使用。除了上述两个场景外,其他同步场景,AQS 都可能作为一个比较完美的方案来使用。
AQS 是一个抽象类,并提供了 互斥 和 共享 两种同步场景。但是由于,AQS 的核心在于维护 state 变量,至于怎么通过 state 来实现 互斥 或者 共享,AQS 并没有过多干涉,只是提供了一些空实现的方法,来让子类具体实现。而且,AQS 提供了对条件变量的支持,可以说是很通用了。
我对与 AQS 的理解目前只到这一步,所以我认为,AQS 比较重要的部分是,如何实现对状态变量的操作和维护,以及中间的一些场景,例如,互斥 和 共享场景是如何实现的;如何支持的条件变量。这些点,也是下文要着重关注的一些点。

1.2、AQS 的继承关系

AQS 继承了 AbstractOwnableSynchronizer 类,AbstractOwnableSynchronizer 这个类提供了互斥的语义,比较简单,这里不做赘述。

二、AQS 的机制和源码

2.1、AQS 类的结构

2.1.1、AQS 中 CLH 队列节点结构

AQS 使用了 CLH 自旋锁,去解决 互斥 和 共享 场景下的等待问题。

CLH CLH(Craig, Landin, and Hagersten locks): 是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。
CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

节点的四个状态:

  • a. CANCELLED = 1:因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收;
  • b. SIGNAL = -1:表示这个结点的继任结点被阻塞了,到时需要通知它;
  • c. CONDITION = -2:表示这个结点在条件队列中,因为等待某个条件而被阻塞;
  • d. PROPAGATE = -3:使用在共享模式头结点有可能处于这种状态,表示锁的下一次获取可以无条件传播;
  • e. 0: None of the above,新结点会处于这种状态。

Node 类的结构:

    static final class Node {
        /** 共享模式 */
        static final Node SHARED = new Node();
        /** 独占模式 */
        static final Node EXCLUSIVE = null;

        /** 状态位:表示线程已经取消的状态值 */
        static final int CANCELLED =  1;

        /** 状态位:表示后一个节点的线程需要唤醒的状态值 */
        static final int SIGNAL    = -1;

        /** 状态位:线程(处在Condition休眠状态)在等待Condition唤醒 */
        static final int CONDITION = -2;

        /**
         * 使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条件传播;
         */
        static final int PROPAGATE = -3;

        /**
         * CLH 节点的状态位
         */
        volatile int waitStatus;

        /**
         * 前驱结点
         */
        volatile Node prev;

        /**
         * 后置节点
         */
        volatile Node next;

        /**
         * 当前线程
         */
        volatile Thread thread;

        // 下一个等待条件(Condition)的节点,由于Condition是独占模式,因此这里有一个简单的队列来描述Condition上的线程节点。
        Node nextWaiter;

        /**
         * 返回是否是共享模式
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * 返回前一个节点,如果为空则抛出NullPointerException。当前任不能为空时使用。可以省略null检查,但它是用来帮助VM的。
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    
        }

        // addWaiter 使用
        Node(Thread thread, Node mode) {     
            this.nextWaiter = mode;
            this.thread = thread;
        }

        // Condition 使用
        Node(Thread thread, int waitStatus) {
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

2.1.2、AQS 的成员变量

// 等待队列的头结点
private transient volatile Node head;
// 等待队列的尾结点
private transient volatile Node tail;
// 状态
private volatile int state;

AQS 有3个比较重要的成员变量。

  • head CLH 队列的头结点
  • tail CLH 队列的尾结点
  • AQS 的状态变量

这3个变量都使用了 volatile 修饰,volatile 可以保证有序性、可见性,但不能保证原子性。为了在多线程下,安全的使用这些变量,AQS 使用了 CAS 来操作这些变量。

2.2、互斥场景的实现

2.2.1、AQS 互斥场景下的相关方法

  • acquire(int arg) 申请获取状态(无法中断)
  • acquireInterruptibly(int arg) 申请获取状态(支持中断)
  • release(int arg) 释放状态

先附一张找到的流程图,帮助理解代码
在这里插入图片描述

下面我们着重看下 acquire(int arg) 和 release(int arg) 这两个方法

2.2.2、acquire(int arg)

acquire(int arg) 源码

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
        	// 生成节点,并加入队列
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 由于 acquireQueued 方法在处理中断的时候,会清空中断状态【parkAndCheckInterrupt 方法】,所以,如果 node 在 acquireQueued 中被阻塞的话,这里需要重新设置阻塞状态
            selfInterrupt();
    }

其中主要有tryAcquire(arg) 、acquireQueued(addWaiter(Node.EXCLUSIVE),arg)、selfInterrupt() 三个操作,下面挨个看下 ,首先是 tryAcquire(arg)

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

很明显这个方法需要由子类自己去实现,实际上,AQS 状态的变化也是在这个方法中完成的,明显的模板方法模式。下面继续看 addWaiter(Node.EXCLUSIVE) :

    private Node addWaiter(Node mode) {
        // 生成节点
        Node node = new Node(Thread.currentThread(), mode);
        // 获取当前的尾结点
        Node pred = tail;
        // 如果尾结点不为 null
        if (pred != null) {
            // 将新节点的前驱引用指向这个尾结点
            node.prev = pred;
            // CAS 将新节点设置为尾结点
            if (compareAndSetTail(pred, node)) {
               // 将原来尾结点的 next 指针指向 新节点
                pred.next = node;
                return node;
            }
        }
        // 执行到这里,说明要么是 尾结点为 null,要么是 CAS 设置尾结点的时候失败了
        enq(node);
        return node;
    }

下面看下 enq(node) 这个方法执行了:

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                 // 如果尾结点 为 null ,那么首先要初始化 head 节点,然后将 tail 节点 指向 head,然后继续循环
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            	// 如果尾结点不为 null 了
            	// 将新节点的 前驱结点 指向 尾结点
                node.prev = t;
                // 设置新节点为 尾结点
                if (compareAndSetTail(t, node)) {
                	// 将尾结点的 next 指针指向 新节点
                    t.next = node;
                    return t;
                }
            }
        }
    }

这样,一个完整的 addWaiter(Node.EXCLUSIVE) 流程就走完了,主要涉及到节点的创建,以及head、tail节点的初始化以及新节点的入队,并不是很麻烦。这个时候,我们回到 acquire(int arg) 方法,继续看 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 。这个方法其实就是让每个节点通过休眠,等待被前置节点唤醒,获取锁。

    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)) {
                 // 如果当前节点的前驱节点是 head 节点,并且成功申请了状态,将进行以下操作:
                 // 将当前节点设置为头节点
                    setHead(node);
                    // 释放 next 指针,帮助 GC 
                    p.next = null; 
                    // 修改标记位
                    failed = false;
                    // 返回中断标记
                    return interrupted;
                }
                // 判断当前节点是否需要阻塞,如果需要,则进行阻塞。否则继续循环。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 如果 判断当前节点需要阻塞 && 阻塞线程后,判断该线程被中断 ,则将中断标记改为 true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

比较重要的是 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 这两个方法,下面一起看下:

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    	// 获取 前置节点的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 如果状态是 SIGNAL ,则说明需要前置节点 pred 来 唤醒 node,直接休眠
             */
            return true;
        if (ws > 0) {
            /*
             *ws >0 说明是 CANCELLED 状态,则一直往前找,直到找到一个节点,而且这个节点的ws 非 CANCELLED 状态的
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            // 舍弃中间的节点
            pred.next = node;
        } else {
            /*
             * waitStatus 一定是 0 或者 PROPAGATE。 这个时候,将前置节点的 ws 设置为 SIGNAL,等到下一次再进入shouldParkAfterFailedAcquire 方法的时候,就可以走 ws == Node.SIGNAL 这个分支了
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

   private final boolean parkAndCheckInterrupt() {
   		// 阻塞当前线程
        LockSupport.park(this);
        // 注意:这里获取中断状态后,会重新清空中断状态
        return Thread.interrupted();
    }

shouldParkAfterFailedAcquire 方法,其实就是根据 node 的前置节点的 waitStatus 状态位,来判断是否需要休眠,当 waitStatus 为 SIGNAL 时,线程将被休眠。这里其实就是 AQS 对 CLH 锁进行的变种,即后置节点并不会自旋,而是进行休眠,等可以申请状态的时候,由前置节点进行唤醒。parkAndCheckInterrupt 方法就很简单了,直接使用 LockSupport.park,将当前线程挂起。

2.2.3、release(int arg)

release 源码:

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

主要关注这几个方法:

  • tryRelease 由子类实现,在这个方法里修改 state
  • unparkSuccessor 唤醒后置节点

由于 tryRelease 需要子类实现,我们主要看下 unparkSuccessor 是如何唤醒后置节点的。
unparkSuccessor 源码:

    private void unparkSuccessor(Node node) {
        // 获取 节点的 waitStatus
        int ws = node.waitStatus;
        if (ws < 0)
            // 如果 ws <0 ,则cas 操作 ws 更新成 0
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * 获取后置节点
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            // 如果没有后置节点,或者后置节点的 waitStatus >0 (为CANCELLED),则 从队列尾部向前遍历找到最前面的一个 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);
    }

unparkSuccessor 代码并不复杂,但是注意,当前节点的后置节点的 waitStatus 为 CANCELLED 时,这时需要找到离当前节点最近的一个非 CANCELLED 状态的节点,这个时候,需要从队尾进行遍历。具体原因可以看下 cancelAcquire 这个方法,这个方法,最后有一行 node.next = node; 相当于将被取消的节点的next 指针指向自己,这个时候如果从 head 遍历,则会出现死循环,而从 tail 开始遍历,则可以正常遍历。

2.3、共享场景的实现

2.3.1、AQS 共享场景下的相关方法

  • acquireShared(int arg) 申请获取状态(无法中断)
  • acquireSharedInterruptibly(int arg) 申请获取状态(支持中断)
  • releaseShared(int arg) 释放状态

我们主要关注 acquireShared(int arg) 、 releaseShared(int arg) 两个方法

2.3.2、acquireShared(int arg)

先附一张流程图(来源:https://ddnd.cn/2019/03/15/java-abstractqueuedsynchronizer/index.html)
在这里插入图片描述

acquireShared 源码

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

acquireShared 调用了 tryAcquireShared 和 doAcquireShared 两个方法,同样的,tryAcquireShared 需要由子类实现,在这个方法中改变state ,下面来看下 doAcquireShared 这个方法做了什么。
doAcquireShared 源码

   private void doAcquireShared(int arg) {
   		// 生成节点,并入队
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 获取前置节点
                final Node p = node.predecessor();
                if (p == head) {
                    // 如果前置节点是 head,则尝试获取状态
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                       //  这个方法会将node设置为head。
                       // 如果当前结点acquire到了之后发现还有许可可以被获取,则继续释放自己的后继, 后继会将这个操作传递下去。这就是PROPAGATE状态的含义。
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // 如果申请状态失败,则进行线程阻塞相关的逻辑
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

与互斥场景下的状态申请相比,acquireShared(int arg) 将 acquireQueued(addWaiter(Node.EXCLUSIVE),arg)、selfInterrupt() 统一写在了doAcquireShared 方法中。但是,不同之处是,setHeadAndPropagate 中,会根据传入的 propagate 进行等待节点的唤醒。下面看下 setHeadAndPropagate 的源码。

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         */
         // 这里应该是判断是否需要唤醒后置节点,但这里的判断我不是很理解,所以保留了原生的doc,下面是我理解的
        //1.propagate > 0 表示调用方指明了后继节点需要被唤醒
        //2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点
       
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                // 唤醒后置的共享节点
                doReleaseShared();
        }
    }

其中的doReleaseShared(); 会唤醒线程,这个方法在下面的 releaseShared(int arg) 一起看。

2.3.3、 releaseShared(int arg)

releaseShared(int arg) 源码

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

releaseShared(int arg) 中调用了 tryReleaseShared 、 doReleaseShared,其中tryReleaseShared 需要子类重写,所以我们只关注 doReleaseShared ,看到底是怎么进行线程唤醒的。
doReleaseShared 源码.

    private void doReleaseShared() {
        for (;;) {
            // 获取当前头结点
            Node h = head;
            if (h != null && h != tail) {
                // 如果头结点不为null + 不是尾结点 + 头结点的waitStatus是SIGNAL+CAS 设置waitStatus成功,就唤醒第二个节点
                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))
                    //如果头结点的waitStatus是0,则 CAS 设置 waitStatus 为 PROPAGATE,不成功,则继续循环
                    continue;                
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

注意下,退出循环的条件是 h == head,这样就会出现如下的情况:
当前 :head->a->b->c->d,此时,a 处于阻塞状态
然后,有状态释放,这个时候,a被唤醒,同时,a成功获取了状态,成为了首节点:head(a)->b->c->d 。由于setHeadAndPropagate 时,会唤醒 a 的后置节点 b,b成功获取了状态,随后通过 setHeadAndPropagate 唤醒了 c,但是,c没有获取到状态,重新回到休眠状态。这个时候 a 释放了持有的状态。来个简易图 (为了表示方便,使用 setHAP 代表 setHeadAndPropagate,使用 doRS 代表 doReleaseShared)
a->setHAP->doRS(此时,head 仍为 a)                                                                  ->releaseShared->doRS(此时,head 为b)
b                                                           ->setHAP->doReleaseShared->(此时,head为b)

很明显,a节点所在的线程,在做 releaseShared 时,队列的 head 并不是它,而是 b。a 所在的线程,唤醒的其实是 c。

2.4、对条件变量(Condition)的支持

AQS 提供了Condition 接口的实现类,ConditionObject,每个 ConditionObject 中都维护了条件队列。在分析 ConditionObject 类之前,我们需要理解 AQS 中的同步队列(syn queue)和 ConditionObject 中的条件队列(condition queue)之间的关系。

2.4.1、同步队列 VS 条件队列

同步队列 和 条件队列的锁状态以及联系

同步队列节点:入队(无锁) —> 队列中(获取锁) —>出队(拥有锁)
条件队列节点:入队(拥有锁) —> 队列中 (释放锁) —> 出队(同其他线程争夺锁)

可以明显的看出,同步队列,入队的时候是线程是没有锁的,但是出队的时候,是拥有锁的;相反,条件队列,入队的时候,线程是拥有锁的,在队列中将锁释放,出队的时候,线程已经不再拥有锁了。

一次普通的请求锁+条件等待过程中,节点和队列的变化:

Created with Raphaël 2.2.0 开始 请求锁 节点进入同步队列 同步队列中获取锁 离开同步队列 节点进入条件队列 条件不成立? 条件队列中释放锁 条件达成(signal),节点加入同步队列 结束 yes no

上述的过程中,除了最后的条件达成(signal)时,条件队列中的节点转移到同步队列之外,基本上同步队列 和 条件队列是没有太多交集的。

2.4.2、ConditionObject 的 类结构

        // 等待队列的开始节点
        private transient Node firstWaiter;

        // 等待队列的尾结点
        private transient Node lastWaiter;

ConditionObject 只有这两个成员变量,即条件队列的首尾节点,很简单

2.4.3、ConditionObject 的等待(await())

看下,await() 方法的实现,await() 支持中断的处理。

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
            	// 检测到中断,直接抛出异常
                throw new InterruptedException();
            // 将节点加入到条件队列中(源码放在这个方法下面了)
            Node node = addConditionWaiter();
            // 释放当前的状态
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
            	// 如果这个节点不在当前在同步队列中,则挂起线程
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 节点加入到 同步队列 中,需要调用 acquireQueued 方法,尝试获取锁
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
            	// 这里已经获取到了锁,说明,节点已经从 同步队列中移除,现在需要把通过 unlinkCancelledWaiters 把节点从 条件队列中移除
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                // 根据interruptMode 处理中断
                reportInterruptAfterWait(interruptMode);
        }
        
private Node addConditionWaiter() {
			// 获取尾结点
            Node t = lastWaiter;
            // 如果最后一个节点被取消了,则清除取消节点
            if (t != null && t.waitStatus != Node.CONDITION) {
                // 清除取消节点(源码放下面了)
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            // 创建节点
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
            	// 初始化头结点
                firstWaiter = node;
            else
                // 将节点放在尾结点后面
                t.nextWaiter = node;
            // 更新尾结点指针
            lastWaiter = node;
            return node;
        }
        
		// 取消取消状态的节点,这个方法需要在持有锁的情况下,才调用,整个过程并不会出现并发情况,也就没有了线程安全的问题
        private void unlinkCancelledWaiters() {
        	// 获取首节点
            Node t = firstWaiter;
            // 保存离 firstWaiter 最近的一个(包含 firstWaiter ),状态为 CONDITION 的节点
            Node trail = null;
            while (t != null) {
                // 获取下一个节点
                Node next = t.nextWaiter;
                if (t.waitStatus != Node.CONDITION) {
                    // 如果节点的waitStatus 不是 CONDITION,就将这个节点清除掉
                    // 将节点的后置指针情况
                    t.nextWaiter = null;
                    if (trail == null)
                    	// 如果当前尚未找到 trail (说明头结点的状态不是 CONDITION )
                        firstWaiter = next;
                    else
                        // 如果已经找到了 trail,则将 trail 的 nextWaiter 指针指向当前节点的下一个节点(跳过了本节点,相当于把本节点踢出了队列)
                        trail.nextWaiter = next;
                    if (next == null)
                       // 如果当前节点已经是最后一个节点了,则更新 lastWaiter 指针
                        lastWaiter = trail;
                }
                else
                    trail = t;
                // 继续下个节点
                t = next;
            }
        }

需要注意的地方:

  • await 方法在获取到互斥锁之后调用
  • 条件成立后,节点直接从条件队列转移到同步队列

2.4.4、ConditionObject 的唤醒(signal())

signal() 源码:

        public final void signal() {
            // 判断线程是否持有锁
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 获取条件队列的首节点
            Node first = firstWaiter;
            if (first != null)
            	// 唤醒首节点(源码在下边)
                doSignal(first);
        }
 
         private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                	// 如果需要唤醒的节点没有后置节点,则直接更新 lastWaiter 指针为null
                    lastWaiter = null;
                // 将first 的 nextWaiter 置为null,帮助GC
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

    // 将节点从条件队列 转移到 同步队列
    final boolean transferForSignal(Node node) {
        /*
         * cas 操作失败(已经被转移),返回false
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * 节点加到同步队列,并返回前驱结点
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        //  如果被取消或者 前驱结点的 CAS 操作失败,则直接唤醒节点
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

2.4.5、图解 await 和 signal 过程中发生的事

在这里插入图片描述

三、参考

  • https://segmentfault.com/a/1190000016462281#item-6-11
  • https://ddnd.cn/2019/03/15/java-abstractqueuedsynchronizer/
  • https://blog.csdn.net/yyzzhc999/article/details/96917878
Python网络爬虫与推荐算法新闻推荐平台:网络爬虫:通过Python实现新浪新闻的爬取,可爬取新闻页面上的标题、文本、图片、视频链接(保留排版) 推荐算法:权重衰减+标签推荐+区域推荐+热点推荐.zip项目工程资源经过严格测试可直接运行成功且功能正常的情况才上传,可轻松复刻,拿到资料包后可轻松复现出一样的项目,本人系统开发经验充足(全领域),有任何使用问题欢迎随时与我联系,我会及时为您解惑,提供帮助。 【资源内容】:包含完整源码+工程文件+说明(如有)等。答辩评审平均分达到96分,放心下载使用!可轻松复现,设计报告也可借鉴此项目,该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的。 【提供帮助】:有任何使用问题欢迎随时与我联系,我会及时解答解惑,提供帮助 【附带帮助】:若还需要相关开发工具、学习资料等,我会提供帮助,提供资料,鼓励学习进步 【项目价值】:可用在相关项目设计,皆可应用在项目、毕业设计、课程设计、期末/期/大作业、工程实训、大创等学科竞赛比赛、初期项目立项、学习/练手等方面,可借鉴此优质项目实现复刻,设计报告也可借鉴此项目,也可基于此项目来扩展开发出更多功能 下载后请首先打开README文件(如有),项目工程可直接复现复刻,如果基础还行,也可在此程序基础上进行修改,以实现其它功能。供开源学习/技术交流/学习参考,勿用于商业用途。质量优质,放心下载使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值