首页 > 编程语言 >Java & Lock & AQS & 总结

Java & Lock & AQS & 总结

时间:2024-10-30 13:19:05浏览次数:3  
标签:状态 非空 Java AQS Lock 线程 共享 唤醒 节点

前言


 相关系列

 涉及内容

概述


 简介

    AQS类是Java设计用于在并发环境下保护资源的API。AQS类全称AbstractQueuedSynchronizer @ 抽象队列同步器类,其“半”实现了以“同步”为核心的线程管理机制,用于对想要/已经访问资源的线程进行等待/保存/唤醒管理以使之达成/解除同步,该线程管理机制被称简称为同步机制。所谓“同步”是由AQS类定义的概念,并且即使在概念中也是极为抽象的存在,原因是其并未像“行走/飞翔/游泳”概念一样被明确,而是类似于“行动”概念一样的再抽象体。如果一定要对“同步”概念进行描述,那么将之大致理解为“规则”是比较准确的,因为达成同步的线程将因为“遵守规则”而实现对受保护资源的安全并发访问。“安全”同样是极为抽象的概念,初学者很容易从数据角度切入而将之片面理解为正确,但由于环境/硬件/需求同样也是程序开发/运行的限制/影响因素,因此“安全”概念实际上也可能基于正确/限流/批次等多种维度被明确。

    AQS类将“安全/同步”概念交由子类明确/实现。由于各子类对“安全”概念的明确不同,并且不同的“安全”概念明确又需要制定相应的规则,即明确/实现相应的“同步”概念予以保证,因此AQS类仅是定义了“安全/同步”概念,而概念的明确/实现则被交由子类负责,故而上文才会说其“半”实现了同步机制。通过对各子类“同步”概念明确/实现的回调,AQS类可以在达成各类同步的同时确保同步机制线程管理功能的统一性。这种编程方式被称为模板模式,Java中基本所有抽象类形式的API都使用了模板模式。AQS类子类对“安全”概念是只需明确而无需实现的,因为其作用仅是为“同步”概念提供明确/实现依据,即我们必须先知道资源对安全访问的实际要求为何,随后才能为之设计相应的访问规则。

    AQS类是J.U.C包下各类线程控制工具的底层实现方案。由于身为抽象类的原因,AQS类并无法直接对外提供服务,但其却是J.U.C包下绝大多数线程控制工具的底层实现方案。需要注意的是:这些线程控制工具本身并不是AQS类的子类,而是通过在内部定义/实现AQS类子类并调用的方式来实现自身设计的,两者之间的区别需要特别注意。此外由于线程控制工具本身还可能存在是否公平等功能选择,因此一个线程控制工具可能不止定义/实现了一个AQS类子类。将AQS类作为底层实现方案的线程控制工具包含但不限于ReentrantLock @ 可重入锁类/ReentrantReadWriteLock @ 可重入读写锁类/Semaphore @ 信号量类/CyclicBarrier @ 栅栏类/CountDownLatch @ 计数器类等。

在这里插入图片描述
 
 

方法


 状态

  • protected final int getState() —— 获取状态 —— 获取当前AQS的状态。

  • protected final void setState(int newState) —— 设置状态 —— 设置当前AQS的状态为指定状态。

  • protected final boolean compareAndSetState(int expect, int update) —— 比较/设置状态 —— CAS设置当前AQS的状态为指定状态。
     

 获取

  • public final void acquire(int arg) —— 获取 —— 令当前线程以独占模式从当前AQS中获取指定数量的状态,如果当前AQS中的状态不存在/不足则无限等待至足够为止。当前线程在等待期间如果被中断不会抛出中断异常,但中断状态会被保留。

  • public final void acquireInterruptibly(int arg) throws InterruptedException —— 可中断获取 —— 令当前线程以独占模式从当前AQS中获取指定数量的状态,如果当前AQS中的状态不存在/不足则无限等待至足够为止。当前线程在等待期间如果被中断会抛出中断异常。

  • public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException —— 尝试纳秒获取 —— 令当前线程以独占模式从当前AQS中获取指定数量的状态,如果当前AQS中的状态不存在/不足则在指定等待时间内有限等待至足够为止并返回true,超出指定等待时间则返回false。当前线程在等待期间如果被中断会抛出中断异常。

  • public final void acquireShared(int arg) —— 获取 —— 令当前线程以共享模式从当前AQS中获取指定数量的状态,如果当前AQS中的状态不存在/不足则无限等待至足够为止。当前线程在等待期间如果被中断不会抛出中断异常,但中断状态会被保留。

  • public final void acquireSharedInterruptibly(int arg) throws InterruptedException —— 可中断获取 —— 令当前线程以共享模式从当前AQS中获取指定数量的状态,如果当前AQS中的状态不存在/不足则无限等待至足够为止。当前线程在等待期间如果被中断会抛出中断异常。

  • public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException —— 尝试纳秒获取 —— 令当前线程以共享模式从当前AQS中获取指定数量的状态,如果当前AQS中的状态不存在/不足则在指定等待时间内有限等待至足够为止并返回true,超出指定等待时间则返回false。当前线程在等待期间如果被中断会抛出中断异常。

 

 释放

  • public final boolean release(int arg) —— 释放 —— 令当前线程以独占模式释放指定数量的许可至当前AQS中,如果完全释放则返回true;否则返回false。

  • public final boolean releaseShared(int arg) —— 释放 —— 令当前线程以共享模式释放指定数量的许可至当前AQS中,如果完全释放则返回true;否则返回false。

 

 检查

  • public final boolean hasQueuedThreads() —— 存在排队线程 —— 判断当前AQS中是否存在正在等待获取状态的线程,是则返回true;否则返回false。

  • public final boolean hasContended() —— 存在竞争 —— 判断当前AQS是否曾被线程竞争过,即判断是否有线程曾在当前AQS中等待获取状态,是则返回true;否则返回false。

  • public final Thread getFirstQueuedThread() —— 获取首个排队线程 —— 从正在当前AQS中等待获取状态的线程中获取最早等待的线程。

  • public final boolean isQueued(Thread thread) —— 是否排队 —— 判断指定线程是否正在当前AQS中等待获取状态,是则返回true;否则返回false。

  • public final boolean hasQueuedPredecessors() —— 存在排队的前驱者 —— 判断当前AQS中是否存在比当前线程更早等待获取状态的线程,是则返回true;否则返回false。

 

 查询

  • public final int getQueueLength() —— 获取队列长度 —— 获取在当前AQS中等待获取状态的近似线程总数。

  • public final Collection getQueuedThreads() —— 获取排队线程集 —— 获取在当前AQS中等待获取状态的近似线程集。

  • public final Collection getExclusiveQueuedThreads() —— 获取独占排队线程集 —— 获取在当前AQS中以独占模式等待获取状态的近似线程集。

  • public final Collection getSharedQueuedThreads() —— 获取共享排队线程集 —— 获取在当前AQS中以共享模式等待获取状态的近似线程集。

 
 

状态


 存在/获取/释放/模式/循环

    AQS类设计子类使用[state @ 状态]作为同步数据的存储介质。虽说“安全/同步”概念的明确/实现被交由子类负责,但AQS类也并非完全没有为之提供实现思路,其推荐子类使用[状态]来记录同步数据。所谓[状态]是指AQS类所组合的int类型字段,虽说各种AQS类子类会根据目标资源的不同而明确/实现不同的“安全/同步”概念,但究其根本就会发现其实现核心大都是对同步“标记”与“计数”的记录,即记录“线程是否已达成同步”及“线程已达成几次同步”。对于前者这是任意数据类型都可以轻易做到的,而后者则通常使用整数类型记录为最佳,因此[状态]便可供子类在实现“同步”概念时统一保存两项关键数据,故而子类对“同步”概念的实现通常无需考虑同步数据的存储介质问题。但需要特别注意的是:AQS类并没有强制子类必须使用[状态]记录同步数据,事实上由于AQS类只在条件机制中绑定了[状态]的读取操作,因此如果子类并无需使用条件机制,则其也完全可以抛弃或设计其它数据存储介质来实现“同步”概念…虽然通常并没有这个必要。

    AQS类子类有义务保证[状态]的正确性。无论是[状态]的获取还是释放,其本质都是对[状态]的赋值行为,而又因为线程获取/释放[状态]的过程可能存在竞争,因此AQS类子类在明确/实现时有义务保证[状态]的正确性。为此AQS类子类往往需要使用CAS来完成对[状态]的赋值,而AQS类也提供了相应的CAS方法以供子类赋值[状态]时调用…当然…这并不是必要的,在已保证线程安全的情况下,对[状态]的赋值也可通过常规方式进行,因此除CAS方法外AQS类也提供了常规的赋值方法以供选择。

    AQS类子类对“同步”概念的明确/实现实际上就是对[状态]存在/获取/释放的明确/实现。所谓[状态]存在是指[状态]的情况是否支持执行获取操作;而获取/释放则通常是指线程在[状态]中记录/清除同步数据的行为,由此我们可知线程达成/解除同步的本质即为[状态]的获取/释放。需要特别注意的是:这里的[状态]并不单指[状态],而是泛指所有AQS类子类的实际同步数据存储介质。只是由于[状态]是AQS类首推的同步数据存储介质,因此便被简称为[状态]的存在/获取/释放。

    AQS类基于独占/共享特性对[状态]的获取/释放进行了两种定义。同步机制存在独占/共享两种模式,即存在独占/共享两套对线程进行管理以使之达成/解除同步的流程,这两种模式的核心差异点具体有三:一是独占模式的[状态]获取/释放必须前后/成对的出现,但共享模式却并无此硬性规定;二是独占模式流程一次只能唤醒一条等待线程,而共享模式流程理论上一次可以唤醒所有等待线程;三是对[状态]的获取必须分别是基于独占/共享特性的实现,即[状态]在独占模式流程中不允许被多线程同时获取,但在共享模式流程中却可以。因此AQS类定义了两类方法用于对[状态]进行独占/共享特性的获取/释放,并分别供以相应的模式流程进行调用。这些方法因为风格被俗称为“两荤两素,四菜一汤”,具体定义/名称/作用/特性如下文所示。需要特别注意的是:AQS类将模式的使用规则全权交给了子类自定义而自身并未进行任何维度的限制,即AQS类子类可根据自身设计自由选择并明确/实现这些方法,因此在子类中两种模式的线程并存或线程兼具两种模式的情况都是可能存在的,也没有以某种模式获取的[状态]就必须以相同的模式释放这种说法…当然目前主流的AQS类子类中似乎还没有这种混合获取/释放的行为…但我们必须明白的是[状态]本身是没有模式概念的,而是[状态]的获取/释放有模式概念。

  • protected boolean tryAcquire(int arg) —— 尝试获取 —— 令当前线程以独占模式尝试获取当前AQS指定数量的状态,成功则返回true;否则返回false。
  • protected boolean tryRelease(int arg) —— 尝试释放 —— 令当前线程以独占模式尝试释放指定数量的状态至当前AQS中,如果彻底释放则返回true;否则返回false。
  • protected int tryAcquireShared(int arg) —— 尝试共享获取 —— 令当前线程以共享模式尝试获取当前AQS指定数量的状态,成功则返回0/正数表示剩余可用状态总数;否则返回负数。
  • protected boolean tryReleaseShared(int arg) —— 尝试共享释放 —— 令当前线程以共享模式尝试释放指定数量的状态至当前AQS中,如果彻底释放则返回true;否则返回false。
  • protected boolean isHeldExclusively() —— 是否独占持有 —— 判断当前AQS是否被当前线程独占,是则返回true;否则返回false。

    AQS类通过循环尝试确保[状态]获取的必然成功。我们可以从“两荤两素,四菜一汤”中发现的是:[状态]的获取尝试并无法保证成功的必然性,对于这种情况AQS类会通过控制线程循环尝试的方式来保证[状态]获取的必然成功,而这也正是同步机制的核心作用。导致[状态]获取尝试失败的原因有很多,或者说是不可数的,但根据实际情况可以将之具体地分为“[状态]存在”及“[状态]不存在”两类。这其中后者并不值得多言,因为在[状态]不支持获取的情况下失败是理所应当的结果。但前者却是值得重点讲述的,因为如果失败不是因为[状态]不存在而导致,则AQS类并不建议子类将该获取尝试直接判定为失败,而是推荐其在尝试方法中嵌套循环尝试至成功或因为[状态]不存在而失败为止…即AQS类不希望[状态]的获取尝试因为除[状态]不存在以外的原因而失败…因为将线程交由同步机制负责循环重试是相当低效的。而也正是因为该原因,获取尝试的失败通常都带有[状态]不存在的隐性含义

    AQS类子类需要确保[状态]释放尝试的必然成功。与[状态]的获取尝试不同,[状态]的释放尝试在定义上是不允许失败的,因为同步的解除理论上不受除达成以外的任何因素影响,甚至在共享模式中也不受同步达成的影响。但由于AQS类子类对“同步”概念的明确/实现以及赋值CAS确实可能导致失败的情况,故而AQS类子类需要人为确保[状态]释放尝试的必然成功,而这通常会在尝试方法中内嵌循环尝试来实现。虽说[状态]的释放尝试没有失败的说法,但其却存在“彻底”的概念,该概念用于表示释放尝试是否可令AQS存在[状态],因为[状态]的释放与存在之间并不是必然关系。这么说确实有些抽象,但我们可以通过以下例子来理解它:如果某AQS类子类规定线程以独占模式获取N次[状态]后也必须释放N次才能解除同步,那么当释放次数少于N次时,虽然其已释放过[状态],但其它线程依然会因为独占特性而无法成功获取,因此此时AQS中依然是不存在[状态]的,这种情况下释放尝试就需要返回false以表示其未彻底释放。而当线程第N次释放尝试解除同步后,由于此时的AQS已支持其它线程达成同步,因此第N次释放尝试就应该返回true以表示其彻底释放了[状态]。由此我们可知[状态]的彻底释放会带有[状态]存在的隐性含义,也因此同步机制会在[状态]彻底释放时唤醒在同步队列中等待的线程
 
 

节点


 线程/模式/前驱/后继

    AQS类支持在线程尝试获取[状态]失败后以Node @ 节点的形式对其进行管理。AQS类中用于管理线程的数据结构被称为同步队列,其本质为遵循FIFO规则的双向引用链表,该知识点会在下文讲解同步队列时详述,此处只需知道节点为同步队列的基本组成单位即可。

    节点是[thread @ 线程]的容器。AQS类会根据模式将线程封装为独占/共享节点后加入同步队列,而节点的模式则可通过[nextWaiter @ 模式/后继等待者]判断。该名称通常会令初学者产生词不达意的疑惑,这是因为其被用于记录模式的同时还被用于持有节点在条件队列中的后继引用。由于只有独占节点才能加入条件队列,因此AQS类出于节省内存的目的复用了该字段,这是一种相对极致的内存优化策略。为了持有条件队列中的后继引用[模式/后继等待者]被设计为节点类型,因此模式实际上也是通过持有不同的节点引用来表示的。AQS类中的静态常量节点<EXCLUSIVE/SHARED @ 独占/共享 @ null/node>被分别用于表示独占/共享模式,由此我们可以认为:当节点位于同步队列中时如果[模式/后继等待者]为<独占>即为独占节点;否则为共享节点。但很遗憾该结论实际上是错误的,因为除了<独占>外,独占节点的[模式/后继等待者]实际上还可能为其它节点引用,因为独占节点可能同时位于同步/条件队列中,该知识点会在下文讲解条件队列时详述。

    节点类在结构上支持持有[prev/next @ 前驱/后继节点]的引用。[前驱/后继节点]的存在使得同步队列支持自由地前/后遍历及访问,因此同步队列本质上是双向链表。
 

 等待状态

    [waitStatus @ 等待状态]是控制/影响节点及其真后继节点所受行为的标记,即AQS类会根据[等待状态]选择对节点/真后继节点的执行行为。所谓节点的真后继节点是指节点的首个未取消直接/间接后继节点,同理可得节点的真前驱节点即为节点的首个未取消直接/间接前驱节点。取消是撤销节点[线程]获取[状态]的操作,该知识点会在下文讲解取消时详述,此处只需知道标准的取消节点是不持有[线程]的空节点且[后继节点]为自身即可。由于节点的[后继节点]可能已被取消,因此节点[等待状态]的控制/影响效果就会自动从身为首个未取消直接后继节点的[后继节点]转移到首个未取消间接后继节点上。[等待状态]是综合状态,即其是由多维度概念掺杂糅合后的产物,这些概念维度包含但不限于“取消”、“位置”及“标记”等。关于真前驱/后继节点及[等待状态]的枚举/含义具体如下:

在这里插入图片描述

<CANCELLED @ 取消 @ 1> —— 表示节点已被取消,即节点[线程]对[状态]的获取已被取消。
 
    [等待状态]为<取消>的节点会断开其与[线程]的引用(即将[线程]置null)成为空节点,并会以将[后继节点]赋值为自身(即后继自引用)的方式断开与[后继节点]的引用,但[前驱节点]会被保留,作用是避免并发线程对同步队列的前遍历中断。
    通常当节点[线程]中断/超时时其节点的[等待状态]便会被赋值为<取消>。当然,也不排除例如OOM等一些非常规异常的影响。

0 —— 表示节点的真后继节点[线程]不需要被唤醒。
 
    0并没有正式的命名,同时也是[等待状态]的初始值。[等待状态]为0意味着节点没有[后继节点],或是[后继节点][线程]尚未进入有限/无限等待状态,又或是真后继节点[线程]将要/正在/已被唤醒。

<SIGNAL @ 信号 @ -1> —— 表示节点的真后继节点[线程]需要被唤醒,即使其可能未处于有限/无限等待状态。
 
    在节点[线程]进入有限/无限等待状态前,必须先将其[前驱节点]的[等待状态]CAS赋值为<信号>,以及在相关涉及到节点重连的场景中也存在将[等待状态]CAS赋值为<信号>的行为。这么做的目的是告知AQS需要唤醒节点的真后继节点[线程]以保证程序的正常运行。
    注意!节点的[等待状态]为<信号>并不意味着其真后继节点[线程]一定处于有限/无限等待状态。有时由于并发的原因,真后继节点[线程]可能避免了进入有限/无限等待状态或进入后又被立即唤醒。但由于这种情况难以/无法被探查且相对少见,因此为了保证真后继节点[线程]的绝对唤醒即使其可能永远都不会进入有限/无限等待状态也会将节点的[等待状态]赋值为<信号>,但后果就是可能带来重复唤醒的性能消耗,该知识点会在下文讲解各块内容时详述。

<CONDITION @ 条件 @ -2> —— 表示独占节点正位于条件队列中。
 
    当独占模式的线程因为条件机制而被封装为独占节点并加入条件队列时,其[等待状态]会被赋值为<条件>。

<PROPAGATE @ 传播 @ -3> —— 保证“共享节点[线程]状态唤醒的传播性”。
 
    共享节点相对于独占节点而言有一个非常有趣的特性:当共享节点[线程]成功获取[状态]后,如果AQS依然存在[状态]则其还需唤醒后续的共享节点[线程]。该特性被称为“共享节点[线程]状态唤醒的传播性”,也是共享模式可以一次唤醒所有等待线程的根本原因,而<传播>便用于维护这种传播性以避免其中断。“共享节点[线程]状态唤醒的传播性”是AQS类中一个具有相当难度的知识点,该知识点会在下文讲解状态唤醒时详述。

 
 

同步队列


 链表/公平

    AQS类使用同步队列保存尝试达成同步失败的线程。同步队列是AQS类用于保存线程的数据结构,当线程尝试同步失败时,AQS类会将线程封装为节点并尾插至同步队列中有限/无限等待。一个值得思考的问题是:既然同步机制会控制线程循环尝试达成同步,那又为什么要将尝试同步失败的线程加入到同步队列中等待呢?实际上该问题的答案在上文中其实已经提及过,即[状态]获取尝试的失败通常都带有[状态]不存在的隐性含义。而在[状态]不存在的情况下,令线程持续不断地进行必然/大概率失败的同步尝试不过只是徒增开销的无意义行为,因此令线程在同步队列中等待实际上是避免无意义开销的有效手段。当[状态]因线程彻底释放而存在,或同步因为中断/超时而取消时,等待中的线程将被信号/中断/超时唤醒并再次/取消尝试同步。

    同步队列是逻辑队列。所谓逻辑队列是指同步队列并不是类似LinkedList @ 链接列表的对象,其本质只是单纯的链表,而AQS类则持有其[head/tail @ 头/尾节点]的引用。由于同步队列的节点类在结构设计上支持持有[前驱/后继节点]的引用,因此AQS类只要持有了[头/尾节点]就相当于持有了整个同步队列。

    同步队列是AQS类为子类提供的公平策略实现方案。同步队列是标准FIFO @ 先入先出队列,线程会从队列的尾部插入,并在同步达成后从头部移除。由于AQS类规定只有位于同步队列头部的线程才具备同步资格,因此在同步队列中同步的达成必然是公平的,即在同步队列中成功达成同步的线程必然是访问时间最早/等待时间最久的。此外虽然AQS类只会在线程尝试达成同步失败时将之插入同步队列中,但是否失败却是由子类全权负责明确/实现的,因此除[状态]不存在而导致的被动失败外,AQS类子类还可以先通过“故意/计划”性质的主动失败令线程在加入同步队列后再进行真正的尝试同步,从而确保线程同步达成的必然公平,因此AQS类子类可通过同步队列实现自身的公平策略。而事实上,所有基于AQS类的API其公平策略(如果存在的话)也确实都是如此实现的…至少我没有发现例外。

    同步队列是低效的。我们其实不难理解这一点,因为无论同步队列中保存了多少线程,按照AQS类的设定也就只有头部线程可以尝试达成同步,因此同步队列中同步实际上就是在单线程环境中达成的,故而性能低下也是可以预见的。而也正是因为该原因,除非[状态]确实不存在,否则正如上文所说AQS类其实并不建议子类将尝试同步失败的线程交由同步机制负责重试,而是推荐其在尝试方法中嵌套循环尝试至成功或因为[状态]不存在而失败为止,因为这将导致线程在[状态]存在的情况下被加入同步队列中有限/无限等待。虽说这并不会对[状态]获取的成功必然性造成影响,但却会对AQS类子类的性能造成严重的损害。毕竟只有在尝试同步必然/大概率失败的情况下,将线程加入同步队列中等待才有益于减少连续尝试造成的性能损失。
 

 插入

    AQS类会将新节点尾插至同步队列作为[尾节点]。该操作流程可具体分为以下三步:

将节点的[前驱节点]赋值为[尾节点] ——> 将[尾节点]CAS赋值为节点 ——> 将旧[尾节点]的[后继节点]赋值为新[尾节点]

    我们需要特别注意第二步并发时可能产生的线程安全问题,即当多个[线程]基于相同[尾节点]试图将所属节点设置为新[尾节点]时可能产生的节点丢失问题。这些节点只有一个会因最后成为[尾节点]而被保留,而其它节点虽然都曾在某一时间成为过[尾节点],但最终还是会被后续的新[尾节点]所覆盖从而失去与GC ROOTS的直接/间接引用导致被GC回收而造成丢失。为了避免这一点,将[尾节点]赋值为节点的行为必须使用CAS进行,以确保基于相同[尾节点]进行的多个[尾节点]赋值行为只有一个能执行成功。执行失败的[线程]会重新读取[尾节点]不断循环重试,直至成功将所属节点加入同步队列为止,该场景的具体情况如下图所示:

在这里插入图片描述

 

 虚拟节点

    同步队列的[头节点]为Dummy Node @ 虚拟节点。虚拟节点是专门作为[头节点]使用的空节点,因此同步队列中持有[线程]的非空节点必然位于虚拟节点之后。首个虚拟节点会在首个节点入队时创建,后续则由[线程]成功获取[状态]的节点充当。虚拟节点通常有简化代码的作用,因为当链表设计有虚拟节点时开发者可以无需在意链表为空的特殊情况而直接以常规逻辑将节点加入链表中,代价则是付出一个节点的额外内存消耗。但同步队列中的虚拟节点并不具备该作用,原因是其首个虚拟节点并不在AQS初始化时创建,而是在首个节点入队时创建的。当节点[线程]尝试将节点加入同步队列中时如果发现同步队列为空,即[尾节点]不存在时则会先创建空节点并CAS加入同步队列中作为首个虚拟节点,随后才会将节点加入同步队列。这其实是AQS类对内存的一种极致优化策略,避免在无节点入队时徒增一个节点的内存消耗。但带来的后果就是程序依然需要在节点入队时对同步队列为空的特殊情况进行处理,这包含但不限于[头/尾节点]的空判断及赋值等。但话虽如此,该设计从整体上对性能还是有益的。因为未设计虚拟节点的链表可能因为节点出队而重新为空,但设计有虚拟节点的链表则通常不会出现该情况。因为虚拟节点虽然会被更新,但一般并不会被彻底移除…当然具体还是看链表设计…因此未设计虚拟节点的链表可能需要对节点入队时链表为空的特殊情况进行多次处理,而同步队列则只需成功执行一次即可。

在这里插入图片描述

    同步队列设计虚拟节点有“配合AQS类对节点[等待状态]的定义”及“确保节点插入/移除的并发安全”两个目的。对于前者:由于节点的[等待状态]会控制/影响真后继节点的所受行为,因此同步队列的每个节点都必须存在[前驱节点],如此虚拟节点便可作为首个非虚拟节点的[前驱节点]而存在;而对于后者:当同步队列仅存在单节点时,节点的并发插入/移除将可能导致新节点丢失的线程安全问题,而虚拟节点的存在则可以避免这种情况,因为其确保了将被移除的节点必然不会是新节点插入时所依赖的[尾节点],构建了实际的数据隔离。因此即使没有关于节点[等待状态]的定义,虚拟节点在无锁/多锁并发链表中也大都是存在的,这两者的典型分别为SynchronousQueue @ 同步队列类及LinkedBlockingQueue @ 链接阻塞队列类等,该知识点会在下文讲解移除时详述。
 

 等待

    节点[线程]成功加入同步队列后会进入有限/无限等待状态。节点[线程]进入有限/无限等待状态是为了避免CPU资源的无谓消耗,毕竟在[状态]不存在的情况下获取是没有意义的,与其不断地重复失败不如令自身等待至[状态]存在后再继续尝试获取以节省资源。但话虽如此,实际上节点[线程]想要进入有限/无限等待状态也只有在各项条件/操作达成的情况下才被允许,否则只能不断重复获取至条件/操作达成或因为[状态]获取成功/线程中断/等待超时等正常/异常原因而结束获取。

    只有不具备[状态]获取权利或尝试获取[状态]失败的节点[线程]才允许进入有限/无限等待状态。所谓不具备[状态]获取权利是指节点不是[头节点]的[后继节点],AQS类规定只有[头节点]的[后继节点][线程]才拥有获取[状态]的权利,而该判断也不是[线程]简单检查节点的[前驱节点]是否为[头节点]就能直接实现的,因为如果发现节点的[前驱节点]为取消节点的话,[线程]就必须先找到节点的真前驱节点以作为新的[前驱节点]。除固定作为[头节点]使用的虚拟节点外,同步队列中的所有其它空节点都没有意义,因为其已经没有[线程]需要再获取[状态]了。因此节点[线程]在判断节点的[前驱节点]是否为[头节点]时需要跳过这些取消节点,否则就可能导致永远没有节点[线程]具备[状态]获取权利的情况。

    [线程]会以所属节点为起点通过前遍历的方式查找真前驱节点。前遍历是同步队列的主要遍历方式,甚至可以说是唯一的遍历方式。上文已经说过旧[尾节点]的[后继节点]赋值发生在新[尾节点]入队/赋值之后,而这就意味着节点的[后继节点]为null并不代表其真的没有后继节点,也可能只是两者的引用关系尚未构建。因此在同步队列中后遍历查找是不可靠的,虽然源码中也确实存在不少访问节点[后继节点]的后遍历行为,但这种行为无论目的达成与否都只有一级,即不会在[后继节点]的基础上继续访问[后继节点],因此严格来说同步队列不存在后遍历。但话虽如此,前遍历却不是高效的遍历方式,因为其天生具备“长遍历”的特性,即终点节点距离起点节点可能非常远,这一点在后续讲述首个非空节点[线程]的唤醒时表现的尤为明显…因为该场景是以[尾节点]作为前遍历起点节点的…为此AQS类在各项流程中都非常注意对节点[前驱/后继节点]的“重指向”,以尽可能避免因为[前驱/后继节点]为取消节点而触发的前遍历。

    [线程]会将所属节点的[前驱节点]赋值为真前驱节点,该行为被称作“等待节点[前驱节点]的重指向”。在节点[线程]前遍历寻找真前驱节点的过程中,如果发现当前遍历节点为取消节点,则会将节点的[前驱节点]指向当前遍历节点的[前驱节点],直至找到真前驱节点为止。这种对节点[前驱节点]的赋值行为会顺势达到将取消节点从同步队列中移除的效果,因为重赋值后取消节点将“可能”不再被其它节点所引用,故而其很快就会因为与GC ROOTS没有直接/间接引用而被GC回收。这种在以起点节点的真前驱节点作为终点节点的前遍历中发生的取消节点清理方式被称为“前驱式清理”,并且只存在于“等待”操作中。而除此之外下文在讲解取消时还会详述另外两种取消节点清理方式,其分别名为“断尾式清理”及“失踪式清理”。

    [线程]会将真前驱节点的[后继节点]赋值为所属节点,该行为被称作“等待节点真前驱节点[后继节点]的重指向”。“等待节点[前驱节点]的重指向”完成后,[线程]会继续将真前驱节点的[后继节点]赋值为所属节点,这行为一方面正如上文所说是为了尽可能避免因为[前驱/后继节点]为取消节点而触发的前遍历;另一方面也是为了避免[后继节点]原本可能指向的取消节点被保留。但注意!真前驱节点[后继节点]原本指向的未必一定是取消节点,因为节点取消时同样会将其真前驱节点的[后继节点]CAS赋值为自身的[后继节点]…虽然取消节点的[后继节点]可能还是取消节点…该知识点会在下文讲解取消时详述。

在这里插入图片描述

    节点[线程]进入有限/无限等待状态前会保证[前驱节点]的[等待状态]为<信号>。当经历过真前驱节点的查找后如果节点[线程]依然不具备[状态]获取权利或者虽然具备权利(即使可能未经历查找)但[状态]获取尝试依然失败,则节点[线程]就会被安排进入有限/无限等待状态。但在正式进入有限/无限等待状态前节点[线程]必须先确保[前驱节点]的[等待状态]为<信号>,因为这是表示节点[线程]需要被唤醒的标记。为了保证节点[线程]在进入有限/无限等待状态后必然可被唤醒,当发现[前驱节点]的[等待状态]不为<信号>时节点[线程]会循环将之CAS赋值至为<信号>为止,该CAS被称为等待CAS。由于节点[线程]只有在[前驱节点][等待状态]原本就为或被赋值为<信号>的情况下才会进入有限/无限等待状态,因此在“等待”步骤中进入有限/无限等待状态的节点[线程]必然是安全的,即必然是可被唤醒的。

在这里插入图片描述

    节点[线程]的“等待”步骤会不断重复执行至其成功获取[状态]或被取消为止。“等待”是至关重要的操作,因为其不仅是节点[线程]获取[状态]的入口,还是节点[线程]安全进入有限/无限等待状态的入口,并且同时还具备“前驱式清理”取消节点的作用。正因如此AQS类会出于“获取[状态]”、“安全等待”及“清理取消节点”三种目的唤醒处于有限/无限等待状态的节点[线程],这其中“安全等待”需要重点提及,因为上文已经说过在“等待”步骤中进入有限/无限等待状态的节点[线程]必然是安全/可被唤醒的,但现实情况是取消可能会破坏这种安全性,即导致原本必然可被唤醒的节点[线程]无法被唤醒,因此才需要强制唤醒节点[线程]以再次执行“等待”步骤来重构安全性。当然,无论具体唤醒目的为某样或多样,“等待”步骤的执行都将同时带来以上三种效果。被唤醒的节点[线程]会从检查自身是否具备[状态]获取权利处重新开始执行“等待”步骤,并在不具备权利或获取[状态]失败的情况下再次进入安全的有限/无限等待状态并无限循环,直至成功获取[状态]或被取消为止。
 

 移除

    AQS类会出于“尝试获取[状态]”或“清理取消节点”的目的唤醒首个非空节点[线程]。出于简洁性目的,下文会将这两种不同目的的节点[线程]唤醒方式简称为“状态唤醒”及“清理唤醒”。需要特别注意的是:除目的性外,“状态唤醒”与“清理唤醒”的最大区别在于“状态唤醒”只针对首个非空节点[线程]进行唤醒,而“清理唤醒”则可以唤醒同步队列中的任意节点[线程],因为“清理唤醒”的本质是唤醒取消节点的真后继节点[线程],而这也包含首个非空节点[线程]在内。该知识点会在下文讲解取消时详述,这里强调这一点是为了避免读者产生“清理唤醒”只能唤醒首个非空节点[线程]的错误理解而与下文内容引起冲突…这一点由作者本人在复习时亲身体会…明明是自己写的文章…

    关于首个非空节点[线程]唤醒的内容是AQS类的最难点,该知识点会在下文讲解唤醒时详述,此处只讲述首个非空节点[线程]被唤醒后所执行的相关操作流程。非空节点是指具体持有[线程]的节点,而在同步队列中除作为虚拟节点的[头节点]外就只有取消节点不会持有[线程],因此首个非空节点实际上就是[头节点]的真后继节点。之所以唤醒首个非空节点[线程]是因为AQS类为了遵守FIFO规则以保证同步队列的公平性规定只有[头节点]的[后继节点][线程]才拥有[状态]的获取权利,而首个非空节点作为[头节点]的真后继节点要么原本就是[头节点]的[后继节点],要么会在其[线程]被唤醒并执行“等待 - 前驱式清理”步骤后成为[后继节点],因此首个非空节点[线程]就是事实上具备[状态]获取权利的线程。除此以外我们还可以发现“前驱式清理”不仅仅只会在节点插入时发生于同步队列的尾部,而是也会在首个非空节点[线程]被唤醒时在同步队列的头部发生。而真正的事实是由于“清理唤醒”可能唤醒同步队列中任意位置的节点[线程],因此“前驱式清理”也可能发生于同步队列的任意位置。

    成功获取[状态]的[线程]会令所属节点/首个非空节点成为新[头节点]。当首个非空节点[线程]获取[状态]成功后,其继续保存在同步队列中就没有意义了,因为同步队列本就设计用于管理[状态]获取尝试失败的线程,因此首个非空节点[线程]会将自身从同步队列中移除,而这也将同时伴随着[线程]容器 —— 节点的移除。将[线程]从同步队列中移除非常简单,直接断开其与所属节点的引用即可。失去了节点作为纽带,线程与同步队列之间没有任何关系可言。但节点的移除则远没有那么“直接”,在同步队列的节点“移除”步骤中作为[前驱节点]的[头节点]会代替首个非空节点被移除,而首个非空节点则会取代其成为新[头节点]。“头部移除”是AQS类中未取消节点的唯一合法/标准移除方式,其完整流程如下所示:

在这里插入图片描述

    首个非空节点成为[头节点]后,会如上文所言断开与[线程]的引用,以满足[头节点]必须是为空/虚拟节点的规定。AQS类之所以如此设计节点的“移除”步骤流程主要基于以下两点原因,前者解释了为什么不直接移除首个非空节点,而后者则解释了为什么由[头节点]代替首个非空节点被移除。

设计所需 —— 节点的[等待状态]包含了节点及其真后继节点所受行为的标记信息,例如<信号>表示需要唤醒节点的真后继节点[线程];0则表示不需要唤醒或已被唤醒;而<取消>则表示节点已被取消等。因此首个非空节点被移除将导致程序无法正确选择对其真后继节点所执行的操作,故而首个非空节点不可以被直接移除。而[头节点]的[等待状态]中虽然也包含了标记信息,但作为其真后继节点的首个非空节点[线程]此时已经成功获取了[状态],因此[头节点][等待状态]中包含的标记信息已经失去了作用,因此是可以被移除的。

数据安全 —— 如果没有虚拟节点作为[头节点]的设计,当同步队列中只有单节点时,节点插入/移除的并发执行可能会产生新节点连同[头节点]一同被移除的线程安全问题。而在存在虚拟节点作为[头节点]的情况下, 由于虚拟节点代替首个非空节点被移除,且代替成为[头节点]的首个非空节点(此时已是空/虚拟节点了)只能被真后继节点[线程]移除,因此在新节点[线程]未加入同步队列并成功获取[状态]的情况下首个非空节点是不可能被移除的,因此就避免了节点丢失的线程安全问题。

在这里插入图片描述
 

 取消/清理

    节点[线程]可能因为中断/超时等原因被取消。取消是撤销节点[线程]获取锁的操作,被取消的节点[线程]会在停止尝试获取锁的同时从同步队列中脱离,即不再受邮戳锁的管理。取消由节点[线程]本身执行,无论何种原因触发的取消都会导致节点[线程]从有限/无限等待状态中唤醒。而因为取消被唤醒的节点[线程]会立即断开与所属节点的引用使之成为空节点,因为取消意味着节点[线程]获取锁的操作被解除,因此继续在节点中保留[线程]是没有意义的,只会导致节点[线程]无法被GC回收。断开连接后的[线程]会以原所属节点为起点前遍历查找其真前驱节点。对于该行为可能产生的疑惑是:既然是要查找真前驱节点,那为什么不直接访问节点的[前驱节点]呢?答案是由于取消可能并发的原因,节点的[前驱节点]可能也是取消节点。这种情况并不罕见,因为在选择有限等待的情况下,开发者通常都会设置相同的等待时间,因此在同步队列中一系列相邻节点都被取消的情况是非常正常的。而这也会导致一种很奇妙的现象,即一系列相邻节点[线程]在取消时找到的真前驱节点实际上都是同个节点。

    [线程]会将所属节点的[前驱节点]赋值为其真前驱节点以缩短其他线程前遍历的路径,该行为被称作“取消节点[前驱节点]的重指向”。[线程]在前遍历查找真前驱节点的过程中会不断更新所属节点的[前驱节点]为当前遍历节点,直至遍历到真前驱节点为止。该行为使得其它线程可以在前遍历时跳过部分无意义的取消节点,从而缩短前遍历的路径以增加整体性能。也是因为前遍历的原因,节点在取消时不可以出于移除/辅助GC等目的将[前驱节点]置null或自引用,因为这可能导致其它并发线程的前遍历被异常中断。

在这里插入图片描述

    一个令人比较奇怪的点是:为什么[线程]不直接将所属节点[后继节点]的[前驱节点]赋值为真前驱节点呢?这么做不但可以比将节点[前驱节点]赋值为真前驱节点更好的缩短前遍历路径,更可以因为断开了[后继节点]对节点的引用而间接达到移除取消节点的作用。这是因为该行为违背了AQS类对于节点[前驱节点]的基本规则:即AQS类不允许节点[前驱节点]被非自身[线程]修改,这可能导致同步队列结构混乱而对前遍历造成影响。前遍历是AQS类对数据正确性的最后保证,因此是不允许出问题的,故而任何涉及到节点[前驱节点]被非自身[线程]修改的行为都会被绝对禁止。正是因为该规则的存在,[线程]不会直接将所属节点[后继节点]的[前驱节点]赋值为真前驱节点。同样也是因为该原因,AQS类也没有提供对节点[前驱节点]进行赋值的CAS方法…虽然目前为止我都尚未模拟出可能导致同步队列结构混乱的场景。

    取消节点的[等待状态]为<取消>。完成“取消节点[前驱节点]的重指向”后,[线程]会将所属节点的[等待状态]赋值为<取消>。<取消>是节点是否已被取消的判断依据,即其它线程会通过查看节点的[等待状态]是否为<取消>来判断其是否已被取消,因此也只有从此刻开始,节点才可以被认为是取消节点。

    位于同步队列尾部的取消节点会被“断尾式清理”。所谓取消节点位于同步队列尾部是指取消节点为[尾节点],由于[尾节点]唯一,因此位于同步队列尾部的取消节点自然也唯一。取消节点为[尾节点]意味着其真前驱节点为最后非空节点,也意味着真前驱节点之后任意数量的节点必然都已被取消,因此将之整体清理可避免取消节点入队。[线程]会通过“断尾式清理”整体移除同步队列尾部的所有取消节点,所谓“断尾式清理”是指将作为最后非空节点的真前驱节点直接CAS设置为新[尾节点],该行为成功就相当于清理了真前驱节点之后的所有取消节点。当然,此时真前驱节点还持有后继取消节点的引用,因此为了实现彻底脱离,取消节点[线程]会继续将真前驱节点的[后继节点]CAS置null。该CAS可能因为新节点的并发入队而失败,但无论成功/失败都代表两者的引用已被断开,因为失败意味着有新节点插入同步队列并成为了真前驱节点的[后继节点],这与将真前驱节点的[后继节点]CAS置null成功后再插入新节点的效果是相同的,因此不需要重试。

在这里插入图片描述

    当“断尾式清理”失败或取消节点位于同步队列头部时,AQS类会使用“前驱式清理”加速取消节点的出队。“断尾式清理”失败是指CAS设置真前驱节点为[尾节点]失败,原因是与新节点入队产生竞争。这种情况下取消节点[线程]不会执行额外的失败补偿机制,因为新节点入队后其[线程]势必要执行“等待 - 前驱式清理”步骤来清理相应的取消节点。当取消节点不位于同步队列尾部或“断尾式清理”失败时,[线程]会判断所属/取消节点是否位于同步队列头部。所谓取消节点位于同步队列头部是指真前驱节点为[头节点],由于一系列相邻取消节点的真前驱节点是相同的,因此位于同步队列头部的取消节点可以存在多个。当取消节点位于同步队列头部时[线程]会出于“清理取消节点”的目的唤醒首个非空节点[线程]/真后继节点[线程]以执行“等待 - 前驱式清理”步骤,该唤醒即为上文所述的“清理唤醒”,而取消节点位于同步队列头部亦为“清理唤醒”的触发条件之一。

    可以发现的是:虽然“断尾式清理”失败及取消节点位于同步队列头部时AQS类都支持执行“前驱式清理”,但实际的执行线程却是不相同的。在“断尾式清理”失败的场景中执行“前驱式清理“的线程为新节点[线程],而在取消节点位于同步队列头部的场景中则为首个非空节点[线程]。而这就可能导致一种情况:即“断尾式清理”失败及取消节点位于同步队列头部的两个场景被同时满足。在这种情况下,新节点[线程]和首个非空节点[线程]实际上为同个线程。由于新节点[线程]在完成“前驱式清理”前不会进入有限/无限等待状态,因此此时取消节点[线程]对同为首个非空节点[线程]的新节点[线程]执行的唤醒实际上是无效的。这种无效唤醒在AQS类中其实很常见,在很多场景(通常是对首个非空节点[线程]的唤醒)中为了确保节点[线程]一定可被唤醒,即使明知其可能不处于有限/无限等待状态也会执行唤醒,这也是<信号>只是表示真后继节点[线程]“可能”需要被唤醒的原因。

在这里插入图片描述

    位于在同步队列内部的取消节点[线程]需要确保真前驱节点的[等待状态]为<信号>。所谓位于同步队列内部是指取消节点不位于同步队列头/尾部的情况,而确保真前驱节点[等待状态]必然为<信号>则是指取消节点[线程]会在真前驱节点[等待状态]不为<信号>的情况下将之CAS赋值为<信号>的行为,该CAS被称为取消CAS,作用是保证取消节点的真后继节点[线程]必然可被唤醒。

    取消节点[线程]会将真前驱节点的[后继节点]CAS赋值为自身的[后继节点],该行为被称作“取消节点真前驱节点[后继节点]的重指向”。在真前驱节点[等待状态]被确保的基础上,取消节点[线程]会将真前驱节点的[后继节点]CAS赋值为自身的[后继节点]。该行为很好理解,依然如上文所言是为了尽可能避免因为[前驱/后继节点]为取消节点而触发的前遍历。可以发现的一点是“等待/取消”步骤在关于节点的重指向流程上非常统一,整体可以归纳为以下三步:

赋值等待/取消节点的[前驱节点]为真前驱节点 ——> 保证(查看/CAS赋值)真前驱节点的[等待状态]为<信号> ——> 赋值真前驱节点的[后继节点]为等待节点/取消节点的[后继节点]

    取消节点位于同步队列内部时可能破坏其真后继节点[线程]的等待安全性。上文已经说过[等待状态]是控制/影响节点及其真后继节点所受行为的标记,而此处由于节点已被取消,因此其真后继节点的所受行为便自然由受取消节点[等待状态]的影响转变为受其真前驱节点[等待状态]的影响,或者说是真前驱节点[等待状态]的控制/影响效果由取消节点转移到了其真后继节点上。关于这一点相信并不难理解,但为什么说位于同步队列内部的取消节点会破坏真后继节点[线程]的等待安全性呢?这是因为超时判断代码位于等待CAS代码的上游,因此当节点[线程]因为超时而取消时其无法保证必然已将真前驱节点的[等待状态]CAS赋值为<信号>,从而导致真后继节点[线程]的等待安全性被破坏,即真前驱节点的[等待状态]无法保证真后继节点[线程]被必然唤醒的情况发生,所以才需要通过令位于在同步队列内部的取消节点[线程]确保真前驱节点的[等待状态]必然为<信号>的方法来重构这种安全性。

    成功确保真前驱节点[等待状态]的取消节点会被保留在同步队列中。取消节点被保留确实是令人疑惑的现象,因为[线程]并非没有将所属/取消节点安全清理的能力,只要出于“清理取消节点”的目的“清理唤醒”真后继节点[线程]即可,因此该情况下取消节点被保留大概率是AQS类基于性能上的考量。真后继节点[线程]的查找/唤醒/等待实际上都“价格”不菲,因为前者很可能要通过前遍历实现,而后两者则是依赖于操作系统的“重量级”操作。很显然AQS类认为对于少数位于同步队列内部的取消节点来说如此高成本的清理并不值得,因此其才会在资源与性能的权衡上选择用空间换时间的方案…关键源码/注释如下文所示:

int ws;
if (pred != head &&
        // ---- 判断真前驱节点是否是[头节点],如果不是则意味着取消节点位于同步队列内部。但该判断不具备可靠性,因为真前驱节点[线程]可
        // 能在这之后成功获取[状态]而使之成为[头节点],因此取消节点是否会被保留还需要进一步的判断/操作。
        ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
        // ---- 判断真前驱节点的[等待状态]已/可被CAS赋值为<信号>,该操作的目的是确保取消节点的[后继节点]在成为真前驱节
        // 点的[后继节点]后必然可被唤醒,因为如果节点是因为超时而取消的话,其[线程]可能未能成功将其[前驱节点]的[等待状态]CAS赋值为
        // <信号>,而<信号>是保证自身必然可被唤醒的标记。
        // ---- 由于此时真前驱节点可能已经成为[头节点],因此可能有执行"状态唤醒"的线程与取消节点[线程]一同修改真前驱节点的[等待状态],
        // 因此该CAS失败可能就意味着真前驱节点可能已经成为[头节点]...当然也可能只是因为一系列相邻取消节点[线程]竞争而失败,这种竞争场
        // 景是无法具体确定的。此外,该CAS也可能在真前驱节点成为[头节点]后执行成功,则就会造成[头节点]的[等待状态]重新转变为
        // <信号>的复杂情况,这增大了"状态唤醒"的复杂度,该情况被称为"[头节点]错误信号"。
        pred.thread != null) {
        // ---- 该判断可查看真前驱节点是否被取消/成为[头节点]...但实际上也是不可靠的,因为真前驱节点完全可以在判断后被取消,并且节点
        // 断开与[线程]的引用也在其成为[头节点]之后,因此该判断只能降低真前驱节点在过程中被取消/成为[头节点]的概率而无法彻底避免。该判
        // 断真正的核心作用其实是为了某些竞争场景下避免首个非空节点[线程]可能无法被"状态唤醒"的问题,具体请看下文详述。
    Node next = node.next;
    if (next != null && next.waitStatus <= 0) {
        // ---- 如果取消节点的[后继节点]存在却未取消才将真前驱节点的[后继节点]CAS赋值为取消节点的[后继节点],之所以在取消的情况下不执
        // 行是为了令真前驱节点的[后继节点]可以重指向一系列相邻取消节点的末位取消节点的[后继节点],即令其重指向一个未取消节点...当然这
        // 也只能增大相关的概率而无法完全保证。
        compareAndSetNext(pred, predNext, next);
    }
} else {
    // ---- 以取消节点作为前遍历的终点,通过执行"清理唤醒"处理不满足上述三条件的情况。虽然相关的竞争场景相对复杂且无法确定,但"清理唤醒"
    // 看下文详述。
    unparkSuccessor(node);
}
// help GC
// 帮助GC

// ---- 位于头部队列头部/内部的取消节点会被设置为后继自引用以避免跨带引用问题以帮助GC。
node.next = node;

    取消CAS可能失败。虽然取消CAS失败的原因必然是因为线程竞争,但竞争线程在身份上却有两种可能:一是一系列相邻取消节点中的其它取消节点[线程];二是执行“状态唤醒”的相关线程。后者是因为其在执行“状态唤醒”时必须先对[头节点]的[等待状态]进行判断/赋值后再决定是否唤醒首个非空节点[线程],而这其中就包括了[头节点]等待状态由0CAS赋值为<传播>的转变…该知识点会在下文讲解唤醒时详述。看到这里可能会产生疑惑:当前场景中取消节点不是位于同步队列的内部么?怎么真前驱节点又变成[头节点]了呢?这是因为真前驱节点可能在经过非[头节点]的判断后因为其[线程]成功获得[状态]而成为[头节点]…因此取消CAS的失败场景实际上存在多种可能且难以/无法被具体探查。

    取消CAS失败的情况由“清理唤醒”统一处理,即使无法确定失败的具体竞争场景。取消CAS失败是除取消节点位于同步队列头部外的另一个“清理唤醒”触发时机,也是“清理唤醒”可以唤醒同步队列中任意节点[线程]根本原因,因为节点取消可以发生在同步队列的任意位置。触发该“清理唤醒”的核心目的是借助真后继节点[线程]唤醒后执行的“等待”步骤来重构其等待的安全性,并顺势完成对取消节点的“前驱式清理”。取消CAS失败的直接后果是无法保证取消节点真后继节点[线程]被必然唤醒,因此直接进行补充唤醒便是最好的处理方式。由于被唤醒后的真后继节点[线程]会执行“等待”步骤,因此可保证其在没有成功获取到[状态]的情况下安全进入有限/无限等待状态,并完成其到真前驱节点范围内的“前驱式清理”。“清理唤醒”在取消CAS失败的情况下非常有效,可以做到对所有失败场景的全覆盖处理,即使我们无法分清实际的失败场景具体为何。

    如果取消CAS因为一系列相邻取消节点[线程]竞争而失败,则理论上取消节点[线程]可以直接执行“取消节点真前驱节点[后继节点]的重指向”。因为一系列相邻取消节点[线程]中的一个必然已经代替取消节点[线程]成功执行了取消CAS,即真后继节点[线程]的等待安全性已经被保证,只是由于失败场景无法被明确探知才导致“重指向”无法被执行而已。这种场景下“清理唤醒”的效果需要再进行场景细化,分为“取消节点依然位于同步队列内部”及“取消节点已转移至同步队列头部”两种。前者“清理唤醒”只能起到“前驱式清理”取消节点的效果,因为该场景下真后继节点[线程]即无法获取[状态]也无需重构等待安全性;而后者由于真后继节点[线程]即为首个非空节点[线程]因此还可以在前者的基础上再促进/加速对[状态]的获取…虽然这通常都因为[状态]不存在而没有卵用。

    取消CAS失败作为“清理唤醒”的触发时机在上述场景中很好的平衡了性能与开销。我们已知由于性能的原因当取消节点[线程]成功确保真前驱节点[等待状态]时其并不会触发“清理唤醒”,因此在取消CAS成功执行情况下无论是单个节点的取消还是一系列相邻节点的取消都不会导致“清理唤醒”被触发,从而使取消节点被保留在同步队列中。虽说少量的取消节点被保留在同步队列中确实是可以被容忍的,但如果数量相对庞大的话就会对资源造成比较严重的浪费,故而此时再执行“清理唤醒”便会扭亏为盈…但“数量相对庞大”又该如何去估算呢?取消CAS失败很好的承担了该作用,虽然我们无法因此得知取消节点的具体数量,但却可以将其作为“数量相对庞大”的标记。因为取消CAS只会因为并发失败,而数量越大的一系列相邻取消节点越会产生更高的并发,从而使得取消CAS失败及“清理唤醒”触发的概率也越高…当然该逻辑只在当前场景中有效。

在这里插入图片描述

    如果取消CAS在有“状态唤醒”执行线程参与的竞争场景中失败,则意味着真前驱节点必然已成为[头节点],即取消节点位于同步队列的头部。该情况下“清理唤醒”可以享受到所有效果,即被唤醒的真后继节点/首个非空节点[线程]可以达成“获取[状态]”、“安全等待”及“清理取消节点”三种作用。综上所述可知无论取消CAS失败的竞争场景如何,“清理唤醒”都可以实现完美处理。

在这里插入图片描述

    真前驱节点[等待状态]被确保并无法保证真后继节点[线程]等待的安全性,因为其无法覆盖名为“[头节点]错误信号”的并发场景。上文中我们一直将真前驱节点[等待状态]的成功确保与真后继节点[线程]的等待安全性划上等意,但实际情况是单纯确保真前驱节点的[等待状态]并无法保证真后继节点[线程]的必然唤醒,原因是在有“状态唤醒”执行线程参与的场景下无法保证取消CAS必然位于“状态唤醒”执行线程赋值真前驱节点/[头节点][等待状态]的CAS的上/中游,而这就可能导致“状态唤醒”执行线程因为真前驱节点/[头节点]的[等待状态]不为<信号>而不唤醒真后继节点/首个非空节点[线程]的情况发生,毕竟<信号>才是表示真后继节点[线程]可能需要被唤醒的标记。

    出于简洁性的目的,我们将在“状态唤醒”中赋值真前驱节点/[头节点][等待状态]的CAS称为唤醒CAS。请注意!与本文中其它命名CAS不同,唤醒CAS是两种CAS的集合而非单个CAS,因为在“状态唤醒”中[头节点][等待状态]存在多种变化,但在“[头节点]错误信号”场景中我们只关注其由0到<传播>的转变。在真前驱节点/[头节点]的[等待状态]为0前提下,当取消CAS位于唤醒CAS上游时由于真前驱节点/[头节点]的[等待状态]可被成功确保,因此该情况下真后继节点/首个非空节点[线程]的等待安全性是可以被保证的;而当取消CAS位于唤醒CAS中游(即并发)时如果是取消CAS执行成功,则情况则与位于上游的情况等同;而如果是唤醒CAS执行成功,则其便将因为真前驱节点/[头节点]原[等待状态]不为<信号>而不唤醒真后继节点/首个非空节点[线程]。但在该情况下由于取消节点[线程]会因为取消CAS失败而执行“清理唤醒”,因此后继节点/首个非空节点[线程]的等待安全性也会因为“清理唤醒”的弥补而得以保证;但这一切在取消CAS位于唤醒CAS下游时发生了变化,因为真前驱节点/[头节点]的[等待状态]完全可以在被唤醒CAS赋值为<传播>后再被取消CAS赋值为<信号>。而由于该情况下“状态唤醒”执行线程会因为真前驱节点/[头节点]的原[等待状态]不为<信号>而不唤醒真后继节点/首个非空节点[线程],因此虽然真前驱节点/[头节点]的[等待状态]最终还是会被取消CAS,但真后继节点/首个非空节点[线程]也已经因为错过了原有的唤醒时机而无法被唤醒。该场景即被称为“[头节点]错误信号”,其具体流程如下表所示:

真前驱节点[线程]取消节点[线程]“状态唤醒”执行线程
T01判断取消节点位于同步队列的内部,即真前驱节点不为[头节点]
T02判断真前驱节点的[等待状态]不为<信号>且 <= 0
T03成功获取[状态]并令真前驱节点成为[头节点]
T04发现真前驱节点/[头节点]的[等待状态]为0,将之CAS赋值为<传播>, 不唤醒真后继节点/首个非空节点[线程]
T05将真前驱节点/[头节点]的[等待状态]CAS赋值为<信号>

    为了解决“[头节点]错误信号”场景导致的真后继节点/首个非空节点[线程]的等待不安全问题,取消节点[线程]在确保真前驱节点的[等待状态]后还会再进行一项“真前驱节点[线程]不为null”的条件判断。如果该条件没有被通过,则[线程]也依然会执行清理唤醒而不会将所属/取消节点保留在同步队列中。该判断条件在直观感受上被用于检测真前驱节点是否被取消或成为[头节点]以降低取消节点被保留的概率,之所以说只是降低概率是因为该判断条件是不可靠的:一是因为真前驱节点完全可以在判断后被取消;二是[头节点]虽然是空节点,但其断开与[线程]的引用却在其成为[头节点]之后,故而该判断条件看起来更像是可有可无的优化项…但实际上该判断条件在“[头节点]错误信号”场景中将起到关键作用,可以令取消节点[线程]触发“清理唤醒”来保证真后继节点/首个非空节点[线程]的等待安全性。但由于其含义与“状态唤醒”的具体流程紧密相关,因此该知识点会在下文讲解唤醒时详述,而此处只需知道在“[头节点]错误信号”场景下取消节点[线程]会触发“清理唤醒”来保证真后继节点/首个非空节点[线程]的等待安全性即可。

    AQS类无法保证一系列相邻取消节点的真前驱节点[后继节点]必然会重指向末位取消节点的[后继节点]。当一系列中的多个相邻取消节点[线程]成功确保真前驱节点的[等待状态]时对真前驱节点[后继节点]的重指向会存在竞争情况,因为同一时间会有多个[线程]都在试图将真前驱节点的[后继节点]重指向/CAS赋值为自身所属/取消节点的[后继节点]。由于成功重指向的取消节点[线程]是难以/无法被控制的,这就导致真前驱节点[后继节点]最终重指向的节点可能依然是取消节点,即重指向为一系列相邻取消节点中的一个。对于该情况AQS类有一定的优化措施,即当取消节点[线程]准备对真前驱节点的[后继节点]进行重指向时如果发现自身的[后继节点]也已被取消则会撤销重指向行为,从而在一定程度上提高末位取消节点[线程]成功重指向的概率。但遗憾的是该优化措施并非绝对有效,即并发可能导致突破该判断的情况出现使得非末位取消节点[线程]成功完成重指向。对此AQS类不做任何额外补充操作,其所造成的结果最终会通过前遍历的方式处理。即如果真前驱节点的[后继节点]被重指向后依然指向取消节点,则当需要访问真后继节点时会通过前遍历的方式查找。

    保留在同步队列中的取消节点可能会被部分“失踪式清理”。需要提前讲明的一点是:除同步队列的内部外取消节点还可能保留在同步队列的头部,因为真前驱节点可能因为其[线程]成功获得[状态]而成为头节点。所谓“失踪式清理”顾名思义,是一种可以令取消节点在不经意间消失的清理方式。“失踪式清理”会在取消节点的“重指向”时发生,并且只会发生于一系列相邻取消节点的场景中,其核心在于“重指向”在重构节点关联的过程中可能会间接断开取消节点的外部引用。以“取消节点[前驱节点]的重指向”举例:如果某取消节点的原[前驱节点]依然是取消节点,则当其进行[前驱节点]重指向后其对原[前驱节点]的引用就会被断开。同理,当真前驱节点发生[后继节点]重指向后,其也会断开对原取消[后继节点]的引用。由于断开后取消节点不再被同步队列中的节点所引用,因此便悄无声息的达到了将取消节点移除的效果。而之所以说“失踪式清理”只能发生在一系列相邻取消节点的场景中,是因为单个取消节点的[后继节点]就是真前驱节点,而两者的引用是不会被断开的,故而就导致单个取消节点和一系列相邻取消节点的末位取消节点无法被“失踪式清理”。

    未被“断尾式清理”的取消节点会被设置为后继自引用。所谓后继自引用是指取消节点的[后继节点]被赋值为自身,这么做的目的是为了利于其在从同步队列中移除后被GC回收。虽然即使维持其原本的[后继节点]也不会导致取消节点在移除后无法被GC回收,但却可能导致更加复杂的跨带引用问题,即为了回收新生代的节点而迫使老年代的Major GC被频繁触发,该问题在基于链表实现的API中非常常见。那为什么不直接将取消节点的[后继节点]置null呢?这是因为[后继节点]为null在同步队列中是[尾节点]的“不可靠”标记,因此除非节点被头部移除,否则非[尾节点]的[后继节点]“理论上”都不允许为null。后继自引用是只有未被“断尾式清理”的取消节点才需要执行的操作,即只有位于同步队列头/内部的取消节点才会被设置为后继自引用。这其中的具体原因是被“断尾式清理”的取消节点通常都是处于新生代的新节点,而位于同步队列头/内部的取消节点处于老年代的概率就会大幅增加,导致引发跨带引用的概率也大幅增加,因此才需要被设置为后继自引用。

在这里插入图片描述
 
 

唤醒


 真后继节点/首个非空节点 —— 查找/并发取消

    AQS类存在“状态唤醒/清理唤醒/虚假唤醒”三种节点[线程]的唤醒方式。在这三种唤醒方式中“虚假唤醒”并不属于AQS类本身的设计,而是属于线程工具类LockSupport @ 锁支持自身的缺陷,其表现为通过LockSupport.park(…)方法进入有限/无限等待状态的节点[线程]可能在毫无理由的情况下被唤醒。但由于AQS类只会在相应的条件循环中调用LockSupport.park(…)方法,这遵循了锁支持类的使用规范,因此虚假唤醒并不会对程序造成异常影响,即被虚假唤醒的节点[线程]最终还是会在“等待”步骤中重新进入有限/无限等待状态。而又因为上文已对节点[线程]被唤醒后的行为及“清理唤醒”的触发时机/效果进行了详述,因此本节对于唤醒的主要内容会聚焦在“状态唤醒”中。但在具体讲解该知识点之前,我们需要先了解一个通用的知识点 —— 真后继节点/首个非空节点的查找。无论是唤醒真后继节点[线程]的“清理唤醒”还是只唤醒首个非空节点[线程]的“状态唤醒”查找都是必须的行为,由于首个非空节点的本质是[头节点]的真后继节点,因此两类查找的逻辑/代码其实都是一致/共用的。而当真后继节点/首个非空节点被查找到后,所谓的唤醒也就是一次LockSupport.unpark(Thread thread)方法调用而已…关键源码/注释如下文所示:

// ---- 优先判断取消节点/[头节点]的[后继节点]是否为真后继节点/首个非空节点,如果不是,则以[尾节点]为起点,取消
// 节点/[头节点]为终点通过前遍历的方式进行查找。沿途覆盖至记录每一个未取消节点,并在抵达终点或无节点可遍历后将最
// 后记录的未取消节点作为真后继节点/首个非空节点。
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;
        }
    }
}

    AQS类会优先判断取消节点/[头节点]的[后继节点]是否为真后继节点/首个非空节点。优先判断取消节点/[头节点]的[后继节点]是否为真后继节点/首个非空节点是AQS类的一种理想化快速查找机制,即乐观的认为取消节点/[头节点]的[后继节点]不为取消节点。这种快速查找机制的准确率相对而言是比较高的,因为上文在讲述前遍历时说过“AQS类在各项操作中都非常注意对[前驱/后继节点]的“重指向”以尽可能避免其为取消节点而触发的前遍历”,而在其后续讲述“等待/取消节点[前驱节点]的重指向”及“等待/取消节点真前驱节点[后继节点]的重指向”也验证了这一点。因此当取消节点/[头节点]的[后继节点]不为取消节点时其即为真后继节点/首个非空节点,否则就需要通过前遍历的方式进行查找。

    当取消节点/[头节点]的[后继节点]为取消节点时AQS类会以[尾节点]为起点,取消节点/[头节点]为终点通过前遍历的方式查找真后继节点/首个非空节点。在前遍历的过程中,线程会覆盖式记录(即后者会覆盖前者)沿途遭遇的所有未取消节点,直至在遍历到终点节点或没有节点可遍历(终点节点可能在前遍历期间被并发移除)后将最后记录的未取消节点作为真后继节点/首个非空节点。值得一提的是当取消节点位于同步队列头部,即“清理唤醒”将唤醒的真后继节点[线程]即为首个非空节点[线程]时其依然会将取消节点作为终点节点,因为这与将[头节点]作为前遍历终点本质上没有区别,其相关场景保证了取消节点与[头节点]间要么没有节点,要么全是取消节点,故而使用取消节点作为前遍历终点不但不会影响对首个非空节点的查找,还能在一定程度上起到缩短前遍历路径的作用,因为取消节点必然位于[头节点]的后方。

    一个值得考虑的问题是:为什么在通过前遍历寻找真后继节点/首个非空节点的过程中不顺势清理沿途遭遇的取消节点呢?毕竟这种已/近完全的前遍历理论上是可以最大限度清理取消节点的。事实上相关原因在上文中已经被阐述过多次,即该操作会违背“AQS类不允许节点[前驱节点]被非自身[线程]修改”的基本规则。

在这里插入图片描述

    AQS类通过传播处理真后继节点/首个非空节点在查找过程中被并发取消的情况。由于并发的原因,真后继节点/首个非空节点可能在查找过程中被取消,即当正式唤醒查找到的真后继节点/首个非空节点其[线程]时可能出现为null的情况。这种唤醒失败是非常严重的问题,虽然在不同的唤醒中造成的直接影响各不相同,但最终都会造成AQS存在[状态]但节点[线程]却无法参与获取的情况,因为旧真后继节点/首个非空节点虽然已被取消,但却未保证新真后继节点/首个非空节点[线程]必然被唤醒。

    AQS类是如何处理该问题的呢?我们首先想到的处理方案通常会是循环,即在唤醒时如果发现真后继节点/首个非空节点[线程]为null则重复执行查找操作,直至成功唤醒为止。该方案看起来有理有据,但实际上是不可行的,因为确定真后继节点/首个非空节点持有[线程]只在判断时有效,即真后继节点/首个非空节点完全可以在通过判断后断开与[线程]的引用。而预先在执行唤醒的线程中持有首个非空节点[线程]也是没有作用的,因为这无法避免取消的发生,而被取消的真后继节点/首个非空节点[线程]即使被唤醒也不会执行“等待”步骤…并且最关键的是唤醒方法LockSupport.unpark(Thread thread)也不会返回唤醒结果,因此执行唤醒的线程实际上根本无法得知自身是否已成功唤醒真后继节点/首个非空节点[线程],因此无论循环多少次都无法避免真后继节点/首个非空节点被并发取消的情况。那AQS类到底采用什么方式避免该问题呢?事实上它什么都没有做…具体原因如果是对“取消”步骤已经非常熟悉的童鞋可能已经猜到了:被取消的节点[线程]会在恰当的时机下触发“清理唤醒”,因此如果真后继节点/首个非空节点真的在唤醒时被取消,则其[线程]自然会去唤醒新真后继节点/首个非空节点[线程],并由新真后继节点/首个非空节点[线程]将这种唤醒不断传播下去,直至成功唤醒为止。

 

 独占 —— 释放/辅助

    “状态唤醒”的流程会因为执行线程所用模式的不同而有所差异。除出于“清理取消节点”的目的唤醒真后继节点/首个非空节点[线程]外,AQS类也会出于“获取[状态]”的目的唤醒首个非空节点[线程],这就是所谓的“状态唤醒”。“状态唤醒”的流程会因为执行线程所用模式的不同而有所差异,而共享线程执行“状态唤醒”的流程则远比独占线程要复杂的多,因为其需要额外维护“共享节点[线程]状态唤醒的传播性”。“共享节点[线程]状态唤醒的传播性”是AQS类当之无愧的最难点,需要花费大量时间及精力进行深度学习,因此本节中我们会先讲述独占线程执行“状态唤醒”的相关流程,其同时也是共享线程执行“状态唤醒”的流程基础。

    独占/共享线程在彻底释放[状态]后都会执行“状态唤醒”。线程会在获取[状态]失败时自封为独占/共享节点加入同步队列中有限/无限等待,直至AQS存在[状态]时被唤醒以再次尝试获取,而[状态]的彻底释放便意味着AQS可能存在[状态]…关键源码/注释如下文所示:

/**
 * 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}.
 * 在独占模式中获取。如果tryRelease返回true,则通过解除阻塞一个或多个线程实现。该方法可用于实现锁的unlock方
 * 法。
 *
 * @param arg the release argument.  This value is conveyed to {@link #tryRelease} but is otherwise uninterpreted
 *            and can represent anything you like.
 *            释放参数。该值被传递至tryRelease方法,但其五其它解释,并且可以代表任意你想的概念。
 * @return the value returned from {@link #tryRelease} 从tryRelease中返回的值。
 */
public final boolean release(int arg) {
    // ---- 当前独占线程首先会尝试释放指定数量的[状态],如果未彻底释放则直接返回false。而如果[状态]彻底释放,则在[头节点]存在且[头节
    // 点]的[等待状态]不为0的前提下执行"状态唤醒"并返回true。注意:由于tryRelease(arg)方法返回true时当前独占线程已经解除了对当前
    // AQS的独占,因此可能会其它独占/共享线程并发执行"状态唤醒"。
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0) {
            unparkSuccessor(h);
        }
        return true;
    }
    return false;
}

/**
 * Wakes up node's successor, if one exists.
 * 唤醒节点的后继节点,如果存在的话。
 *
 * @param node the node
 * @Description: ----------------------------------------------------------- 名称 -----------------------------------------------------------
 * 启动后继节点
 * @Description: ----------------------------------------------------------- 作用 -----------------------------------------------------------
 * 唤醒指定节点的真后继节点线程。
 * @Description: ----------------------------------------------------------- 逻辑 -----------------------------------------------------------
 * ---- 当前方法是"取消/状态唤醒"的公用底层方法,作用是唤醒指定节点的真后继节点线程。当唤醒类型为"清理唤醒"时
 * 指定节点即为取消节点;而当唤醒类型为"状态唤醒"时指定节点即为[头节点]。
 * ---- 方法首先会判断指定节点的[等待状态]是否 < 0,即是否为<信号>/<传播>,是则
 * 将之CAS赋值为0,并且不在意该CAS的执行结果。这段代码根据唤醒类型与线程模式的不同其含义也是不同的。当唤
 * 醒类型为"清理唤醒"时该代码没有任何意义,因为取消节点时其[等待状态]必然为<取消>;而当唤醒
 * 类型为"状态唤醒"时则还需要根据线程的模式再次进行场景。
 * ---- 当唤醒类型为"状态唤醒"时,如果线程以独占模式工作,则该代码负责将[头节点]的[等待状态]由SIGNAL(-1:信
 * 号)CAS赋值为0,即唤醒CAS;而如果线程以共享模式工作,则该代码负责将[头节点]的[等待状态]由SIGNAL(-1:信
 * 号)/<传播>发现/还原为0,即还原CAS。还原CAS本质上是一种优化行为,执行成功意味着包含设
 * 置该<传播>的传播共享线程在内之前所有线程释放的[状态]只要没有被并发[获取]就一定能被当前唤
 * 醒共享线程查找/唤醒的首个非空节点[线程]查询到,从而有助于首个非空节点[线程]自身更精确的判断是否继续执行"状
 * 态唤醒"而减少对<传播>的依赖。又因为还原CAS是优化行为,因此对于独占线程来说该行为是可以
 * "状态唤醒"的流程中移除的,因此自然也就无需在意执行的结果,而这也就导致了兼容该代码的独占线程即使唤醒CAS
 * 失败也可以查找/唤醒首个非空节点[线程]。
 * ---- 无论指定节点的[等待状态]是否被CAS赋值,是否CAS赋值成功,在这之后方法都会唤醒指定节点的真后继节点[线
 * 程]。方法首先会判断指定节点的[后继节点]是否存在及取消。不存在或已取消则从[尾节点]/指定节点为起/终点进行前
 * 遍历,并覆盖式记录沿途遇到的所有未取消节点,直至遍历到指定节点或无节点可节点可遍历为止。最后记录的未取消
 * 节点会被视作指定节点的真后继节点。
 * ---- 在真后继节点存在的情况下,方法会将之唤醒。
 */
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try to clear in anticipation of signalling.  It is OK if this fails
     * or if status is changed by waiting thread.
     * 如果状态为负数(即可能需要发送信号),尝试清理预料到的信号。如果这失败了或者状态被等待线程【这里特指共
     * 享模式下】所改变也是允许的。
     */

    // ---- 当前方法是"取消/状态唤醒"的公用底层方法,作用是唤醒指定节点的真后继节点线程。当唤醒类型为"清理唤醒"
    // 时指定节点即为取消节点;而当唤醒类型为"状态唤醒"时指定节点即为[头节点]。
    // ---- 方法首先会判断指定节点的[等待状态]是否 < 0,即是否为<信号>/<传播>,是
    // 则将之CAS赋值为0,并且不在意该CAS的执行结果。这段代码根据唤醒类型与线程模式的不同其含义也是不同的。
    // 当唤醒类型为"清理唤醒"时该代码没有任何意义,因为取消节点时其[等待状态]必然为<取消>;而
    // 当唤醒类型为"状态唤醒"时则还需要根据线程的模式再次区分场景。
    // ---- 当唤醒类型为"状态唤醒"时,如果线程以独占模式工作,则该代码负责将[头节点]的[等待状态]由SIGNAL(-1:信
    // 号)CAS赋值为0,即唤醒CAS;而如果线程以共享模式工作,则该代码负责将[头节点]的[等待状态]由SIGNAL(-1:
    // 信号)/<传播>发现/还原为0,即还原CAS。还原CAS本质上是一种优化行为,执行成功意味着包
    // 含设置该<传播>的传播共享线程在内之前所有线程释放的[状态]只要没有被并发[获取]就一定能被
    // 当前唤醒共享线程查找/唤醒的首个非空节点[线程]查询到,从而有助于首个非空节点[线程]自身更精确的判断是否继
    // 续执行"状态唤醒"而减少对<传播>的依赖。又因为还原CAS是优化行为,因此对于共享线程来说该
    // 行为是可以从"状态唤醒"的流程中移除的,因此自然也就无需在意执行的结果,而这也就导致了兼容该代码的独占线程
    // 即使唤醒CAS失败也可以查找/唤醒首个非空节点[线程]。
    int ws = node.waitStatus;
    if (ws < 0) {
        compareAndSetWaitStatus(node, ws, 0);
    }
    /*
     * Thread to unpark is held in successor, which is normally just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual non-cancelled successor.
     * 线程启动所有的后继节点,这种情况下通常只是下个节点【即[后继]中记录的节点】。但如果[后继]已被取消或明显为
     * null,则从[尾]向前遍历找到实际未取消的后继节点。
     */

    // ---- 无论指定节点的[等待状态]是否被CAS赋值,是否CAS赋值成功,在这之后方法都会唤醒指定节点的真后继节点[线
    // 程]。方法首先会判断指定节点的[后继节点]是否存在及取消。不存在或已取消则从[尾节点]/指定节点为起/终点进行
    // 前遍历,并覆盖式记录沿途遇到的所有未取消节点,直至遍历到指定节点或无节点可节点可遍历为止。最后记录的未
    // 取消节点会被视作指定节点的真后继节点。
    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);
    }
}

    当[头节点]的[等待状态]为<信号>时独占/共享线程会尝试将之CAS赋值为0。该CAS即为上文所提唤醒CAS的另外一种,其作用是告知其它并发执行“状态唤醒”的独占/共享线程当前首个非空节点[线程]不需要被唤醒,因此当独占/共享线程发现[头节点]的[等待状态]为0时便不会再查找/唤醒首个非空节点[线程]。那是不是说在经过[头节点][等待状态]的条件判断后就不存在并发查找/唤醒首个非空节点[线程]的情况了呢?不是的,条件判断只能降低并发查找/唤醒的概率而无法彻底避免并发,只不过并发并不会对AQS类的整体流程造成异常影响罢了,下述表格中列举了多独占线程并发执行“状态唤醒”的部分场景及可能造成的情况:

独占线程A独占线程B首个非空节点[线程]
T01释放[状态]
T02判断[头节点]的[等待状态]为<信号>
T03获取[状态]成功,又释放[状态]
T04判断[头节点]的[等待状态]为<信号>
T05/06CAS赋值[头节点]的[等待状态]为0并在成功/失败后查找/唤醒首个非空节点[线程]CAS赋值[头节点]的[等待状态]为0并在成功/失败后查找/唤醒首个非空节点[线程]
T07/08被独占线程A/B唤醒,并被独占线程B/A重复唤醒
独占线程A独占线程B首个非空节点[线程]第二个非空节点[线程]
T01释放[状态]
T02判断[头节点]的[等待状态]为<信号>
T03获取[状态]成功,又释放[状态]
T04判断[头节点]的[等待状态]为<信号>
T05/06CAS赋值[头节点]的[等待状态]为0并在成功/失败后查找/唤醒首个非空节点[线程]CAS赋值[头节点]的[等待状态]为0并在成功/失败后查找/唤醒首个非空节点[线程]
T07被独占线程A唤醒
T08获取[状态]成功并令首个非空节点成为[头节点],第二个非空节点成为首个非空节点
T09因为所属节点成为首个非空节点而被独占线程B唤醒</font>
T10获取[状态]失败并再次进入有限/无限等待状态

    根据上表中的举例可知:[头节点]的[等待状态]判断无法保证并发查找/唤醒首个非空节点[线程]的情况不存在,而并发则会导致首个非空节点[线程]被重复唤醒及唤醒非首个非空节点[线程]的情况发生。但在这两种情况中前者只会因为线程上下文切换而造成些许额外的性能损失;而后者则不过会因为尝试获取[状态]失败而使得非首个非空节点[线程]再次进入有限/无限等待状态。无论是哪种后果都不会影响到AQS类的整体流程,因此AQS类之所以在一定程度上避免查找/唤醒首个非空节点[线程]的并发主要是基于性能上的考量。

    为什么AQS类不彻底杜绝查找/唤醒首个非空节点[线程]的并发呢?因为从上表中展示的案例来看唤醒CAS失败就意味着已有其它独占/共享线程在查找/唤醒首个非空节点[线程],那么当前独占/共享线程完全可以在唤醒CAS失败后拒绝执行查找/唤醒来彻底避免并发…该方案看似合理实际上并不可行,因为其无法避免取消带来的并发唤醒,即无法避免取消节点位于同步队列头部时其[线程]触发的“清理唤醒”。“清理唤醒”没有在执行前判断/赋值[头节点][等待状态]的说法,甚至于[头节点]就根本不在其操作范围中,因为在其看来所谓的首个非空节点与常规的真后继节点没有任何区别,毕竟其唤醒真后继节点[线程]的目的本就不是为了获取[状态]。除此以外,“[头节点]错误信号”场景也可能导致并发查找/唤醒首个非空节点[线程]的情况发生,因为其会将[头节点]的[等待状态]再次取消CAS赋值为<信号>,从而造成“状态唤醒”的并发。

    由此我们可知AQS类并非不想彻底杜绝查找/唤醒首个非空节点[线程]的并发,而是基于当前设计而言这是难以/无法做到的。可AQS类又为什么在“状态唤醒”中允许唤醒CAS失败的独占线程继续查找/唤醒首个非空节点[线程]呢?毕竟其虽然无法彻底避免并发,但至少是可以和条件判断一样做到降低并发概率的…实际上这是为了在代码上兼容共享线程所做出的取舍,因为两者在对首个非空节点[线程]的查找/唤醒上使用的是同一方法,只是独占线程用于唤醒CAS的代码被共享线程用作了对<信号>/<传播>的发现/还原而已,而其自身用于唤醒CAS的代码则是独立的,该知识点会在下文详述。此外在经过相关的条件判断后实际能够并发查找/唤醒首个非空节点[线程]的独占线程其实已经不多了,少数的线程并发在某种程度上还可以被动辅助/加速首个非空节点[线程]的查找/唤醒,因此重复唤醒带来的也不都是负面影响,这也是AQS类允许唤醒CAS失败的独占线程继续查找/唤醒首个非空节点[线程]的重要原因。
 

 共享 —— 传播/BUG/释放/获取/辅助

    成功获取[状态]的共享节点[线程]会在AQS依然存在[状态]的情况下“状态唤醒”共享首个非空节点[线程],该特性被称为“共享节点[线程]状态唤醒的传播性”。我们可以通过“共享节点[线程]状态唤醒的传播性”得知除释放[状态]外共享节点[线程]还会在成功获取[状态]后执行“状态唤醒”,AQS类之所以会为共享节点[线程]设计该特性是为了令在同步队列中等待的共享节点[线程]可以在存在[状态]时共同参与获取,而非只能像独占线程一样依次上阵,毕竟AQS类是允许线程以共享模式共同获取[状态]的。需要格外注意的是:共享节点[线程]外的共享线程并不存在传播性的说法,即未加入同步队列的共享线程在获取[状态]后并不会“状态唤醒”共享首个非空节点[线程]。

    所谓共享首个非空节点[线程]是指共享模式的首个非空节点[线程]。共享节点[线程]在维护“共享节点[线程]状态唤醒的传播性”时只会唤醒共享模式的首个非空节点[线程],这是非常容易理解的,因为此时的独占线程必然无法获取已被共享线程获取的[状态],因此唤醒独占共享首个非空节点[线程]是没有意义的。需要格外注意的是:“状态唤醒”本身在查找/唤醒首个非空节点[线程]时并没有模式的限制,只唤醒共享首个非空节点[线程]单纯只是“共享节点[线程]状态唤醒的传播性”的行为,其通过在“状态唤醒”的基础上额外添加模式判断来实现这一点。

    “共享节点[线程]状态唤醒的传播性”是“状态唤醒”在共享模式中远难于独占模式的根本原因,AQS类实现了相当复杂/晦涩的代码来保证“状态唤醒”在共享节点[线程]中正常/低耗/高效的传播,该知识点会在下文详述。此外下文也将同步讲述有关<传播>的具体内容,如果说“共享节点[线程]状态唤醒的传播性”是AQS类的最难点,则<传播>就是“共享节点[线程]状态唤醒的传播性”的最难点。

    出于简洁性的目的,下文会将独占/共享线程执行的“状态唤醒”简称为独占/共享“状态唤醒”。在正式对共享“状态唤醒”流程进行讲解之前我们需要先对AQS类旧版本(JDK1.6及其之前)的相关内容进行了解,因为当前版本的AQS类之所以会复杂到如此令人咋舌的地步很大程度上也和修复旧版本的BUG有很大关系…关键源码/注释如下文所示:

public final boolean releaseShared(long arg){
    if (tryReleaseShared(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0){
            // ---- 查找/唤醒首个非空节点[线程]。
            unparkSuccessor(h);
            reture true
        }
    }
    return false
}

private void setHeadAndPropagate(Node node, int propagate) {
    // ---- 将成功获取[状态]的共享首个非空节点[线程]设置为[头节点],并断开与[线程]的连接以使之成为空/虚拟节点。
    setHead(node);
    // ---- 如果可用状态存在且共享首个非空节点的[等待状态]不为0则继续执行"状态唤醒"以维护“共享节点[线程]状态唤醒的传播性”。
    if (propagate > 0 && node.waitStatus != 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            // ---- 查找/唤醒首个非空节点[线程]。
            unparkSuccessor(node);
    }
}

    以上两段是共享线程在释放/获取[状态]后执行“状态唤醒”的关键代码,这其中需要重点关注的是setHeadAndPropagate(Node node, int propagate)方法,其作用是在[线程]成功获取[状态]后将所属/共享节点替换为[头节点]并“状态唤醒”共享首个非空节点[线程]。在正式查找/唤醒共享首个非空节点[线程]前方法会先执行少许的简单判断,作用包含“确定AQS存在[状态]”,“确定首个非空节点[线程]需要唤醒”及“确定首个非空节点[线程]为共享线程”等等。我们可以很明显的发现在旧版本AQS类中共享节点[线程]执行“状态唤醒”的流程并不复杂,甚至完全可以称之为“简陋”,只是单纯在独占“状态唤醒”流程的基础上新增了极少量的代码用来维护“共享节点[线程]状态唤醒的传播性”而已。那是什么原因导致代码会演变为如今这种情况呢?答案是以上代码包含一个可能导致共享首个非空节点[线程]延迟/永远无法被唤醒的BUG…

    旧版本AQS类对“共享节点[线程]状态唤醒的传播性”的维护存在延迟。所谓延迟是指共享节点[线程]在成功获取[状态]后无法保证在存在[状态]的情况下继续执行“状态唤醒”,但又不会造成共享首个非空节点[线程]永远无法被唤醒的情况出现,因为其最终还是会被其它“状态唤醒”所唤醒…相关场景如下:

共享线程A共享线程B共享首个非空节点[线程]
T01释放[状态],并发现旧[头节点]的[等待状态]为<信号>
T02将旧[头节点]的[等待状态]由<信号>CAS赋值为0,并查找/唤醒共享首个非空节点[线程]
T03被共享线程A唤醒,并成功获取[状态]且查询到[状态]不存在,但尚未令共享首个非空节点成为新[头节点]
T04释放[状态],并发现旧[头节点]的[等待状态]为0,因此不查找/唤醒共享首个非空节点[线程]
T05令共享首个非空节点成为新[头节点],由于成功获取[状态]后未查询到[状态],因此虽然AQS实际存在[状态]也依然不执行“状态唤醒”,导致“共享节点[线程]状态唤醒的传播性”的维护出现延迟
T06释放[状态],并发现新[头节点]/共享首个非空节点的[等待状态]为<信号>
T07将新[头节点]/共享首个非空节点的[等待状态]由<信号>CAS赋值为0,并查找/唤醒新共享首个非空节点[线程]

    维护出现延迟实际上已经破坏了“共享节点[线程]状态唤醒的传播性”的定义,但由于共享首个非空节点[线程]最终还是会被唤醒,因此这并不会对程序的运行造成异常影响,故而其并不是促使AQS类更新迭代至新版本的根本原因,其根本原因是“延迟唤醒”还可能进一步导致共享首个非空节点[线程]永远无法被唤醒:通常我们对线程使用[状态]的方式总是保持着“先获取后释放”的认知,因为这确实是独占线程对[状态]的标准使用规范。但该规范对共享线程来说并不适用,因为AQS类并没有在共享线程的该方面进行任何设计上的强制要求,也就是说无论AQS类子类如何定义共享线程对[状态]的使用规则,在AQS类层面共享线程对[状态]的获取/释放都是完全独立的,即即使按“只获取不释放”、“先释放后获取”及“只释放不获取”等规则令共享线程使用[状态]也是完全可行的。但由于作者早期和我们一样都保持着惯性思维,又或者作者原本也并没有打算如此设计,因此旧版本AQS类对共享线程使用[状态]的灵活性支持是不够的…而共享首个非空节点[线程]永远无法被唤醒的BUG就恰恰因此而发生。

    信号量类是基于AQS类共享模式实现的API,被设计用于在多线程环境中进行限流,线程只有在成功从之获取到许可的情况下才允许对受保护的资源进行访问。在其内部实现的AQS类子类中允许线程直接释放[状态]/许可,但不允许线程在无[状态]/许可的情况下直接获取[状态]/许可,因此除非在创建信号量时预设了非零数量的初始[状态]/许可,否则线程只能在自身或其它线程释放[状态]/许可后才能获取到[状态]/许可。共享首个非空节点[线程]永远无法被唤醒的BUG正是因为信号量类定义的这种[状态]使用方式而被暴露出来的…相关复现代码及场景如下所示:

public class TestSemaphore {
    
    /**
     * 信号量实例,初始[状态]为0
     */
    private static Semaphore sem = new Semaphore(0);
    
    private static class Thread1 extends Thread {
        @Override
        public void run() {
            // ---- 获取一个[状态](该方法不支持中断)。
            sem.acquireUninterruptibly();
        }
    }
    
    private static class Thread2 extends Thread {
       @Override
       public void run() {
           // ---- 释放一个[状态](需要注意的是,对于Semaphore而言,即使没有获取许可也是可以释放的,因此acquire/release的本质更类似于
           // 减/加而不是持有,没有必须先获取[状态]才能释放的说法)
           sem.release();
       }
   }

   public static void main(String[] args) throws InterruptedException {
       // ---- 大数量循环以触发BUG场景。
       for (int i = 0; i < 10000000; i++) {
           // ---- 线程1/2专用于获取许可。
           Thread t1 = new Thread1();
           Thread t2 = new Thread1();
           // ---- 线程3/4专用于释放许可。
           Thread t3 = new Thread2();
           Thread t4 = new Thread2();
           // ---- 线程开启。
           t1.start();
           t2.start();
           t3.start();
           t4.start();
           // ---- 阻塞主线程。
           t1.join();
           t2.join();
           t3.join();
           t4.join();
           // ---- 输出循环次数。
           System.out.println(i);
        }
     }
  }
  
}
共享线程1共享线程2共享线程3共享线程4
T01/02获取[状态],因为[状态]不存在而加入同步队列并进入有限/无限等待状态获取[状态],因为[状态]不存在而加入同步队列并进入有限/无限等待状态
T03释放[状态],并发现旧[头节点]的[等待状态]为<信号>
T04将旧[头节点]的[等待状态]由<信号>CAS赋值为0,并查找/唤醒共享线程1
T05被共享线程3唤醒,并成功获取[状态]且查询到[状态]不存在,但尚未令共享节点1成为新[头节点]
T06释放[状态],并发现旧[头节点]的[等待状态]为0,因此不执行“状态唤醒”
T07令共享节点1成为新[头节点],但由于成功获取[状态]后未查询到[状态],因此此时AQS虽然存在[状态]也依然不执行“状态唤醒”,导致共享线程2永远无法被唤醒。

    “共享节点[线程]状态唤醒的传播性”是共享首个非空节点[线程]被唤醒的最后保证从上述场景中我们可以发现共享首个非空节点[线程]永远无法被唤醒虽然是因为“延迟唤醒”及“共享线程获取/释放[状态]的独立性”两方面原因综合导致的,但核心原因还是因为“延迟唤醒”导致“共享节点[线程]状态唤醒的传播性”被破坏,否则成功获取[状态]的共享节点[线程]理论上是必然可在AQS存在[状态]的情况下“状态唤醒”共享首个非空节点[线程]的。共享首个非空节点[线程]永远无法被唤醒是非常严重的BUG,因为这不仅代表着其自身,还同时意味着同步队列中的所有其它独占/共享节点[线程]也都无法被唤醒,由此我们可知“共享节点[线程]状态唤醒的传播性”是共享首个非空节点[线程]被唤醒的最后保证。这并不是说共享首个非空节点[线程]只能被获取[状态]的共享节点[线程]“状态唤醒”,而是指即使没有其它线程继续释放[状态],共享首个非空节点[线程]也至少还能被获取[状态]的共享节点[线程]唤醒。共享首个非空节点[线程]永远无法被唤醒的BUG如今依然被收录在Java的BUG历史中,相应地址如下:

    https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6801020

    目前网上有很多文章在描述这个BUG时会用类似“后继节点会阻塞/hang住,导致延迟唤醒/无法被唤醒”一类的说法,这个说法不能算错,但不严谨。因为从上述内容中我们知道其实“无法唤醒”和“延迟唤醒”的情况都是存在的,只不过“无法唤醒”才是真正导致AQS类更新迭代的主因,而“延迟唤醒”更多只是顺势而为罢了…毕竟“只要代码能跑就不要去动它”这条铁律对大佬也是有效的。

    在了解旧版本AQS类在维护“共享节点[线程]状态唤醒的传播性”上的BUG及产生原因后,接下来正式讲述新版本AQS类关于共享“状态唤醒”的内容。我们首先会对其标准流程进行详细的阐述,随后再阐述其如此设计的原因…关键源码/注释如下文所示:

/**
 * Releases in shared mode.  Implemented by unblocking one or more threads if {@link #tryReleaseShared} returns true.
 * 在共享模式中释放。如果tryReleaseShared方法返回true则通过解锁一个或更多线程实现。
 *
 * @param arg the release argument. This value is conveyed to {@link #tryReleaseShared} but is otherwise
 *            uninterpreted and can represent anything you like.
 *            释放参数。该值被传递给tryReleaseShared方法但没有其它解释,并且可以代表任意你想要的【概念】。
 * @return the value returned from {@link #tryReleaseShared} 从tryReleaseShared中返回的值
 */
public final boolean releaseShared(int arg) {
    // ---- 当前共享线程释放所有[状态]后会调用doReleaseShared()方法执行"状态唤醒"。
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

/**
 * Release action for shared mode -- signals successor and ensures propagation. (Note: For exclusive mode, release
 * just amounts to calling unparkSuccessor of head if it needs signal.)
 * 关于共享模式的发布活动 -- 传递信号至后继节点并确定传播。(注意:关于独占模式,发布就等于调用头节点的
 * unparkSuccessor(),如果它需要传递信号。)
 *
 * @Description: ----------------------------------------------------------- 名称 -----------------------------------------------------------
 * 释放共享
 * @Description: ----------------------------------------------------------- 作用 -----------------------------------------------------------
 * 唤醒当前AQS同步队列共享首个非空节点线程。
 */
private void doReleaseShared() {
    /*
     * Ensure that a release propagates, even if there are other in-progress acquires/releases.  This proceeds in the
     * usual way of trying to unparkSuccessor of head if it needs signal. But if it does not, status is set to
     * PROPAGATE to ensure that upon release, propagation continues. Additionally, we must loop in case a new node
     * is added while we are doing this. Also, unlike other uses of unparkSuccessor, we need to know if CAS to reset
     * status fails, if so rechecking.
     * 确保释放传播,即使有其它获取/释放正在执行。如果需要传递信号,尝试对头节点执行unparkSuccessor方法(这
     * 通常以常规的方式进行)。如果不需要【传递信号】,则将状态设置为<传播>以确保释放后传
     * 播继续【即当前线程释放共享状态后可以保证后续线程也释放】。此外,我们必须循环以防止在我们执行该操作期
     * 间有新节点添加。还有,与unparkSuccessor()方法的其它调用不同,我们需要知道CAS重设状态是否失败,如果失
     * 败则需要重新检查。
     */

    // ---- "状态唤醒"的整体操作需要在死循环中执行,一是令共享线程在唤醒CAS失败时重试;二是避免在"状态唤醒"过
    // 程中有新节点并发入队等待导致传播中断的情况发生。
    for (; ; ) {
        // ---- "状态唤醒"首先会判断[头节点]是否存在且是否不与[尾节点]相同,否则说明当前同步队列中只有虚拟节点
        // 存在,而由于虚拟节点是空节点,因此唤醒也就谈起,方法会跳转至循环的尾部判断是否退出循环。
        Node h = head;
        if (h != null && h != tail) {
            // ---- 如果[头节点]存在且不为[尾节点],"状态唤醒"会判断[头节点]的[等待状态]是否为<信号>,
            // 是则意味着首个非空节点[线程]需要被唤醒。方法首先会将[头节点]的[等待状态]<信号>CAS赋
            // 值改为0,该行为的作用是告知其它并发独占/共享线程首个非空节点[线程]已无需再唤醒以避免重复唤醒。
            // 为了保证这一点当该CAS失败时共享线程需要跳转至开头重新循环,而如果执行成功则当前共享线程将调用
            // unparkSuccessor(Node node)方法查找/唤醒首个非空节点[线程]。
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
                    // loop to recheck cases
                    // 循环检查情况
                    continue;
                }
                unparkSuccessor(h);
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
                // ---- 如果[头节点]的[等待状态]为0,说明存在其它独占/共享在并发"状态唤醒"首个非空节点[线程]。此时
                // 共享线程会尝试将[头节点]的[等待状态]由0CAS赋值为<传播>,用于向被唤醒的首个非
                // 空节点[线程] 表示有共享线程在其被唤醒及获取[状态]期间释放了[状态],作为首个非空节点[线程]用于维
                // 护"共享节点[线程]状态唤醒的传播性"的判断条件之一。

                // loop on failed CAS
                // CAS失败时循环
                continue;
            }
        }
        // ---- 如果[头节点]发生变化,意味着并发入队的新共享节点[线程]可能出现无法被唤醒的情况,需要再次执行以唤醒;否
        // 则直接退出。
        if (h == head) {
            // loop if head changed
            // 如果头节点改变则循环。
            break;
        }
    }
}

    共享线程可能会多次执行“状态唤醒”。我们已知共享线程会在获取/释放[状态]时触发“状态唤醒”,但这里的多次指的是“状态唤醒”在一次触发中多次执行,因为共享线程会在循环中执行“状态唤醒”。AQS类如此设计主要有两个原因:一是确保唤醒CAS可以在失败时重试;二是通过再执行避免某些打破“共享节点[线程]状态唤醒的传播性”的情况发生。由于第二点涉及共享节点[线程]在获取[状态]后的“状态唤醒”执行判断,因此该知识点会在下文讲解执行判断时详述。

    成功将[头节点][等待状态]由0CAS赋值为<信号>的共享线程将获得查找/唤醒首个非空节点[线程]的权利,该共享线程被称为唤醒共享线程。与独占线程不同,共享线程不接受任意唤醒CAS的失败,即共享线程会在该唤醒CAS失败的情况下重新检测[头节点]的[等待状态]以重复执行,而不会等同独占线程只需通过状态判断就必然执行首个非空节点[线程]的查找/唤醒,无论唤醒CAS是否成功。这么做的目的是为了节省开销,因为与独占线程不同,共享线程允许并发的特性会导致首个非空节点[线程]在高并发情况下被大量重复查找/唤醒,而查找/唤醒的成本是相当高昂的。对于独占线程来说其释放[状态]后的并发量通常不会太高,因此少量的并发反而有助于辅助/加速查找/唤醒,也有助于其与共享线程进行竞争。

    共享线程会尝试将[头节点][等待状态]由0CAS赋值为<传播>,成功执行该唤醒CAS的共享线程被称为传播共享线程。该唤醒CAS只在共享“状态唤醒”流程中存在,[头节点][等待状态]为0意味着首个非空节点[线程]不需要被唤醒,因此该情况下共享线程不会去查找/唤醒首个非空节点[线程],而是会将[头节点]的[等待状态]CAS赋值为<传播>。该行为/状态与维护“共享节点[线程]状态唤醒的传播性”有关,粗略且不准确的说法是:当共享节点[线程]成功获取[状态]后如果发现旧[头节点]的[等待状态]为<传播>,则即使其未查询到[状态]也要执行“状态唤醒”,以预防其未能查询到并发释放的[状态]而导致“共享节点[线程]状态唤醒的传播性”中断的情况,该知识点会在下文详述。

    唤醒共享线程在查找/唤醒首个非空节点[线程]前“可能”会将[头节点]的[等待状态]由<信号>/<传播>重新CAS赋值为0,该CAS被称为还原CAS。该行为在上文讲解独占“状态唤醒”流程时实际上已经在源码/详述中提及过,即共享线程会将独占线程的唤醒CAS代码用于对<信号>/<传播>的发现/还原,具体可以看上文中截取的关键源码/注释。通过结合旧版本的源码我们可知独占/共享线程的唤醒CAS如今在代码上已不再共用,而独占/共享线程的唤醒/还原CAS才是实际上的相同代码,只是因为在不同模式的“状态唤醒”中发挥了不同作用才有不同的命名。这种发现无法保证必然性,因为当[头节点]的[等待状态]被相关线程并发CAS赋值为<信号>/<传播>时唤醒共享线程可能已经执行到发现代码的下游。并且在成功发现的基础上也无法保证一定还原,因为同一时间可能存在其它独占线程/共享线程/取消节点[线程]对[头节点][等待状态]进行并发CAS赋值。但这并不会对共享“状态唤醒”的流程造成影响,因为发现/还原本质是优化行为,即使将之从流程中撤销也只会增加开销/拉低性能而不会造成错误,因此也就导致兼容代码的独占线程即使唤醒CAS失败也会查找/唤醒首个非空节点[线程]…事实上由于发现/还原中的发现代码对独占线程来说是一层额外的判断,因此独占线程可能根本不执行唤醒CAS就直接查找/唤醒首个非空节点[线程]。

    我们已知唤醒共享线程是在执行“状态唤醒”时将[头节点][等待状态]由<信号>成功CAS赋值为0的共享线程,故而其能够在流程中再次发现<信号>自然是因为“[头节点]错误信号”场景的“功劳”,而<传播>则是因为传播共享线程的作用。唤醒共享线程对两者的发现/还原属于相同性质的优化行为,目的都是尽力避免对首个非空节点[线程]造成非必要(重复/无效)唤醒。对于<信号>而言,成功发现/还原可以避免重复唤醒,因为唤醒共享线程本身的任务就是查找/唤醒首个非空节点[线程],因此<信号>发现/还原失败则意味着可能/已经有其它线程重复查找/唤醒首个非空节点[线程]。而对于<传播>而言成功发现/还原可以避免无效唤醒,而想要理解这一点我们就必须先知道<传播>及其被成功发现/还原的具体含义。

    <传播>的直接作用是表示AQS“可能”存在[状态]。由于共享线程会在彻底释放[状态]后执行“状态唤醒”,又因为传播共享线程会在“状态唤醒”中将[头节点]的[等待状态]由0CAS赋值为<传播>,因此[头节点]的[等待状态]为<传播>可以间接表示AQS可能存在[状态]。由于上文在提及“共享节点[线程]状态唤醒的传播性”时说过获取[状态]后的共享节点[线程]会在AQS存在[状态]时执行“状态唤醒”,因此共享节点[线程]会将<传播>视作有其它共享线程在其被唤醒及获取[状态]期间并发释放[状态]的标记,从而将<传播>连同其对[状态]的查询结果一起作为是否执行“状态唤醒”的判断依据,该知识点会在下文详述。那为什么说<传播>只表示AQS“可能”存在[状态]呢?具体原因有二:一是共享线程允许并发,因此被并发释放的[状态]也可能被并发获取;二是除释放[状态]外共享线程还可能在获取[状态]后执行“状态唤醒”,因此并非一定有[状态]随着<传播>的产生而释放。基于以上原因,当唤醒共享线程因为发现[头节点][等待状态]为<传播>而继续执行“状态唤醒”时可能造成共享首个非空节点[线程]的无效唤醒,即唤醒后并无[状态]可获取的情况。

    <传播>被成功发现/还原意味着位于该还原CAS上游释放的[状态]“只要依然存在”就必然可被唤醒共享线程查找/唤醒的首个非空节点[线程]直接查询到。唤醒共享线程对首个非空节点[线程]的查找/唤醒发生在对<传播>的发现/还原之后,因此<传播>被成功发现/还原意味着位于该还原CAS上游释放的[状态]只要未被并发获取就必然可被唤醒共享线程查找/唤醒的首个非空节点[线程]直接查询到。因此当被唤醒的首个非空节点[线程]为共享模式时其完全可以在获取[状态]后直接通过查询到的[状态]来维护“共享节点[线程]状态唤醒的传播性”,从而减少借助<传播>而造成的无效唤醒现象。为什么只是减少无效唤醒现象而非完全避免呢?因为并发获取的情况在直接查询到[状态]的场景中也是依然存在的。

在这里插入图片描述

    “共享节点[线程]状态唤醒的传播性”的难点在于维护。所谓维护是指确保获取[状态]后的共享节点[线程]在AQS存在[状态]的情况下必然“状态唤醒”共享首个非空节点[线程],这听着玄乎实际上并不困难,只需令共享节点[线程]在获取[状态]后无条件执行“状态唤醒”即可,但后果就是令本就高流量的“状态唤醒”并发被非必要的加剧。所谓非必要加剧是指“状态唤醒”只会造成非必要唤醒,无条件执行之所以会造成这种情况是因为其执行时机伴随的是[状态]的获取而非释放,因此当[状态]被获取一空时“状态唤醒”便会无效唤醒。而重复唤醒对于共享线程来说通常不需要考虑,由于其“状态唤醒”流程不允许唤醒CAS失败,因此只要没有“清理唤醒”参与,单纯的共享“状态唤醒”并发并不会出现真正意义上的重复唤醒。

    我们虽然已知“状态唤醒”的并发无论在何种模式中都是存在的,但这并不意味着其可以被无条件的允许。“状态唤醒”是成本高昂且执行耗时的操作,因此如果某“状态唤醒”只会造成非必要唤醒,那么出于性能/开销上的考量其就不应该/推荐被执行。这一点对于共享线程来说尤为重要,因为其特性注定了其并发必然远高于独占线程。因此如果说独占线程还能从非必要唤醒的负面影响中获得辅助/加速唤醒的正向收益,那对于共享线程来说就纯粹是性能/开销上的双重损失。基于上述原因,成功获取[状态]的共享节点[线程]应该选择性地执行“状态唤醒”以维护传播“共享节点[线程]状态唤醒的传播性”,而这种选择性就是维护的难点所在。AQS类设计了繁多且晦涩的判断条件以“尽可能”阻止“状态唤醒”的非必要执行,因此接下来的内容核心就在于阐述这些判断条件的具体设计思想及作用…关键源码/注释/详解如下文所示:

/**
 * Sets head of queue, and checks if successor may be waiting in shared mode, if so propagating if either propagate
 * > 0 or PROPAGATE status was set.
 * 设置队列头,并检查后继节点是否在共享模式下等待,如果是这样,则在设置了propagate > 0或propagate状态时进行传播。
 */
private void setHeadAndPropagate(Node node, int propagate) {
    // Record old head for check below
    // 记录旧头用于下方检查
    
    // ---- 将[线程]已成功获取[状态]的共享首个非空节点设置为[头节点],并断开与[线程]的链接。
    Node h = head;
    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.
     * 如果传播由调用者指示,或者通过先前的操作(例如 h.waitStatus在setHead之前或之后)被记录(注意:这里使用等待状态的符号检查,
     * 因为传播状态可能会被转换为信号),并且下个节点在共享模式中等待,或者我们不知道,因为它看起来为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.
     * 两种检查的保守型可能导致非必要的重复唤醒,但只在有多竞争获取/释放时,因此大多数情况下可以立即或很快就发送信号。
     */
     
    // ---- 判断是否存在可用状态,再隐式判断[头节点]的[等待状态]是否为<信号>或<传播>,
    // 有任意一点满足则继续执行"状态唤醒"以维护"共享节点[线程]状态唤醒的传播性"。这些判断条件的具体作用请看下文章详解。
    if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
        // 执行"状态唤醒"以维护"共享节点[线程]状态唤醒的传播性"。
        Node s = node.next;
        if (s == null || s.isShared()) {
            doReleaseShared();
        }
    }
    
}

    为了阐明具体作用,本文不会按代码顺序对判断条件进行讲解,而是会根据其目的的“必要性”由浅入深的阐述,由于某些判断条件之间极其相似,因此在阅读下文内容时请时刻注意判断条件的序号。

判断条件二:h == null & 判断条件四:(h = head) == null
 
    判断条件二/四用于判断旧/新[头节点]是否不存在,所谓旧/新[头节点]是指共享节点[线程]在不同时期的[头节点]。旧[头节点]是共享节点为共享首个非空节点时的[头节点],即共享节点的[前驱节点]。由于共享节点[线程]执行条件判断二时其已被移除,因此被称为旧[头节点];而新[头节点]则是共享节点[线程]执行条件判断四时的最新[头节点],请注意该新[头节点]并不一定是共享节点,因为在共享节点[线程]维护传播的这段时间里可能已有多个共享首个非空节点被移除。
    旧/新[头节点]为null的情况理论上是不存在的,因为只要曾经有节点入队就至少会有虚拟节点存在于同步队列中。而如果从未有过节点入队则当前流程也不会触发,因此该判断其实更多是以防万一或为后续改版做准备,因为在高并发API中很多情况连作者本身都无法明晰。这种情况并不算罕见,例如在LinkedTransferQueue @ 链接迁移队列类中也有类似对不会为null的头节点进行判null的操作。

判断条件五:h.waitStatus < 0
 
    判断条件五用于查看新[头节点]的[等待状态]是否 < 0,其本质是判断首个非空节点[线程]是否需要被唤醒。我们已知当[头节点]的[等待状态] >= 0时是不需要执行“状态唤醒”的,因为前者将伴随着“清理唤醒”;而后者则意味着首个非空节点[线程]不存在或已将/被唤醒,因此判断条件五是AQS类用于阻止“状态唤醒”被非必要执行的最后保证。当条件判断五也不被通过时,共享节点[线程]将出于“重复唤醒”的角度拒绝“状态唤醒”,因此在前五个判断条件中只有判断条件五才是避免“状态唤醒”被非必要执行的唯一必要条件,而其它判断条件则是以加速/辅助等优化性质存在的。
    看到这里可能会产生疑惑,因为“共享节点[线程]状态唤醒的传播性”在定义上就强调“状态唤醒”只在[状态]存在时执行,并且上文也确实讲述了包含<传播>在内诸多用于确定[状态]存在的内容,那么最后怎么会将重复唤醒作为避免“状态唤醒”非必要执行的最终依据呢?而且还是在上文说过共享“状态唤醒”并不存在真正意义重复唤醒的情况下。事实上这确实是令人难以理解/接受,因为源码/资料/本文都花了太多的篇幅来实现/讲解确定[状态]存在的相关操作,以至于令读者产生了[状态]必然是可被确定存在的错觉。而现实情况是虽然有相关措施辅助,但在AQS并发环境中确定[状态]存在的行为实际上是不可实现的,因为无论是直接/间接查询到的结果都只是[状态]快照而非[状态],而不与源数据保持同步/相等的快照通常是无法代替源数据作为可靠判断依据的。
    快照并非完全无法代替源数据作为可靠的判断依据,但这要么要求快照与源数据保持同步/相等,例如被synchronized关键字修饰保护;要么保证其所受操作在性质上不与判断条件相左,例如对只能递增的数据进行小于初始值的判断。但很显然[状态]快照不符合上述任何一种场景,因此[状态]的查询结果不能作为避免“状态唤醒”非必要执行的最终依据。而虽然共享“状态唤醒”并发并不会导致真正意义上的重复唤醒,但执行与不执行毕竟还是有区别的,而既然已经确定首个非空节点[线程]不需要被唤醒,那么“状态唤醒”自然也不应该再被继续执行。

    共享节点[线程]对“状态唤醒”非必要执行的拦截可能导致共享首个非空节点[线程]永远无法被唤醒。判断条件五虽然以重复唤醒为最终依据避免了“状态唤醒”的非必要执行,但同时也造成共享首个非空节点[线程]永远无法被唤醒的情况,因为这可能导致并发入队后成为共享首个非空节点[线程]的新共享节点[线程]失去被唤醒的机会。由于我们已知“共享节点[线程]状态唤醒的传播性”是共享首个非空节点[线程]被唤醒的最后保证,因此如果共享首个非空节点[线程]错失了被共享节点[线程]“状态唤醒”的时机,那就可能永远都无法再被唤醒了。共享首个非空节点[线程]永远无法被唤醒的并发场景相当复杂,不仅其本身发生的条件相对极端,并且还要先绕过判断条件一到四的保护,因此为了便于理解该情况此处会先给出只考虑判断条件五的简化版本,完整的并发场景会在讲解完前五个最重要的判断条件后展示。

[状态]变化释放共享线程获取共享线程共享节点[线程]A共享节点[线程]B共享节点[线程]C
T011 --> 1“状态唤醒”共享节点[线程]B
T021 --> 0获取[状态]并查询到[状态]快照为0
T030 --> 4释放[状态]
T044 --> 4令共享节点B成为新[头节点],并因为新[头节点][等待状态]为0而不执行“状态唤醒”
T054 --> 3获取[状态]成功且不执行“状态唤醒”因为竞争获取[状态]失败而加入同步队列,共享节点C成为新共享首个非空节点
T063 --> 2获取[状态]成功且不执行“状态唤醒”再次因为竞争获取[状态]失败,将新[头节点][等待状态]由0CAS赋值为<信号>
T072 --> 1获取[状态]成功且不执行“状态唤醒”再次因为竞争获取[状态]失败,进入有限/无限等待状态

    AQS类通过令共享线程循环执行“状态唤醒”来弥补判断条件五导致共享首个非空节点[线程]永远无法被唤醒的问题。上文在刚开始讲述共享“状态唤醒”时就已经说过其会在触发后于循环中被多次执行,目的是在确保唤醒CAS必然成功的同时避免某些“共享节点[线程]状态唤醒的传播性”被打破的情况发生,而如今我们可知该情况即由条件判断五及新共享节点的并发入队导致。通过循环执行我们可以令错过“状态唤醒”时机的新共享节点[线程]/共享首个非空节点[线程]得到重新被“状态唤醒”的机会,其具体作用/场景会在后续随共享首个非空节点[线程]永远无法被唤醒的完整并发场景一起展示。

判断条件一:propagate > 0 & 判断条件三:h.waitStatus < 0
 
    判断条件一/三用于判断直接/间接查询到的[状态]是否存在。其中propagate是共享节点[线程]获取[状态]后直接查询到的[状态]快照,即tryAcquireShared(int arg)方法的返回值。当[状态]快照 > 0时意味着AQS“可能”存在[状态],因此共享节点[线程]需要继续执行“状态唤醒”以维护“共享节点[线程]状态唤醒的传播性”。我们已知[状态]快照是不可靠的,因此不能作为准确的判断条件,需要结合其它判断条件继续判定;而h.waitStatus < 0则是对旧[头节点][等待状态]的综合判断,具体表现为<信号>与<传播>两种情况,但本质都是对[状态]间接查询结果的判断。
    在判断条件三中<传播>的情况是比较好理解的,其产生是因为被传播共享线程唤醒CAS且未被唤醒共享线程发现/还原。<传播>意味着AQS“可能”存在[状态],因此共享节点[线程]应该继续执行“状态唤醒”以维护“共享节点[线程]状态唤醒的传播性”。但对于<信号>可能就不那么好理解,因为其原意是指共享节点[线程](当时还是共享首个非空节点[线程])需要被唤醒,但此时其显然已经被唤醒了,而已出队的旧[头节点][等待状态]又不可能继续向后发挥标记作用,因此共享首个非空节点[线程]需要在此条件下执行“状态唤醒”就显得莫名其妙。但实际上这并不难理解,因为该<信号>很可能是由“[头节点]错误信号”场景得来的。上文说过“[头节点]错误信号”场景会将[头节点]的[等待状态]由0/<传播>CAS赋值为<信号>,因此该<信号>的前身就很有可能是<传播>,如此共享节点[线程]在旧[头节点]的[等待状态]为<信号>时也要继续执行“状态唤醒”就变得可以理解了。当然,如果该<信号>的前身是0的话这也会造成重复唤醒。
    判断条件五与判断条件三完全一致,只是判断对象由旧[头节点]切换为了新[头节点],但两者在性质上却天差地别,因为判断条件五是基于重复唤醒角度进行的判断,而判断条件三则是基于[状态]存在角度进行的判断。但话虽如此,两者在场景上却极为相似,判断条件五在判断条件三的基础上只额外新增了一种<信号>的场景,即新[头节点]的<信号>不是被“[头节点]错误信号”设置而是在成为新[头节点]时就存在的,只不过未曾被执行“状态唤醒”的独占/共享线程唤醒CAS赋值为0而已。

    旧[头节点]的<传播>并不一定能被共享节点[线程]发现。直接查询[状态]是不可靠的,而<传播>是用于查询[状态]的间接/辅助手段,因此如果其能够保证可靠,则判断条件四/五将不复存在。但遗憾的是<传播>显然也是不可靠的,因为其终究只能表示[状态]曾经“可能”被释放过而无法保证其尚未被获取。而实际情况是<传播>可能比你目前所知的还要不可靠,因为其可能无法被共享节点[线程]在判断条件三处发现,原因是传播共享线程的唤醒CAS发生在判断条件三的下游…因此<传播>即无法表示[状态]必然存在,也无法表示[状态]必然不存在…<传播>存在但未被共享节点[线程]发现的场景是新版本导致共享首个非空节点[线程]永远无法被唤醒的原因之一,因此该场景会在下文展示在共享首个非空节点[线程]永远无法被唤醒的完整并发场景中。

    <传播>的核心作用是加速对共享首个非空节点[线程]的唤醒。一个值得考虑的问题是:既然只有判断条件五是“可靠”的,那为什么还要设置不可靠的判断条件一/三呢?关于这问题的原因上文其实已经说明过,即判断条件五可能导致共享首个非空节点[线程]永远无法被唤醒。虽然该情况已经通过循环执行共享“状态唤醒”的方式进行了保障处理,但毫无疑问这会对共享首个非空节点[线程]的唤醒造成延迟,并且这种延迟会随着并发量的增加而愈发严重,因为在高并发环境中新节点的出/入队会非常频繁,从而导致共享首个非空节点[线程]被延迟唤醒的情况也越发频繁。为了优化上述情况,AQS类在判断条件五的基础上额外设立了不可靠的判断条件一/三,目的是从[状态]存在的角度增大“状态唤醒”的必要执行概率并尽可能避免直接将判断条件五作为“状态唤醒”的执行依据,从而减少延迟唤醒情况的发生。而虽然这种不可靠的[状态]直接/间接查询结果会导致共享首个非空节点[线程]的无效唤醒,但在经过精密的逻辑设计后其在程序整体运行中获得的效果却是优大于劣的。

    在讲述完“状态唤醒”执行最重要的五个判断条件后,接下来会展示共享首个非空节点[线程]永远无法被唤醒的完整并发场景。请着重关注表中红/紫色的加粗内容,其是导致/修复异常情况的关键内容。

[状态]变化释放共享线程获取共享线程共享节点[线程]A共享节点[线程]B共享节点[线程]C
T012 --> 1获取[状态]并查询到[状态]快照为1,令共享节点A成为旧[头节点]
T021 --> 1因为[状态]快照为1而通过判断条件一,执行“状态唤醒”。锁定旧[头节点],并将之[等待状态]由<信号>CAS赋值为0,再查找/唤醒共享首个非空节点[线程]B
T031 --> 0获取[状态]并查询到[状态]快照为0
T040 --> 4释放[状态]后执行“状态唤醒”,锁定旧[头节点]
T054 --> 4令共享节点B顶替旧[头节点]成为新[头节点]
T064 --> 4发现“状态唤醒”前/后的[头节点]不一致,循环/补充执行“状态唤醒”进行“状态唤醒”执行判断的前三部分:

[状态]快照为0 - 判断条件一不通过
旧[头节点]存在 - 判断条件二不通过
旧[头节点][等待状态]为0 - 判断条件三不通过
T074 --> 4将已移除的旧[头节点][等待状态]由0CAS赋值为<传播>发现“状态唤醒”前/后的[头节点]不一致,循环/补充执行“状态唤醒”
T084 --> 4发现“状态唤醒”前/后的[头节点]不一致,循环/补充执行“状态唤醒”继续进行“状态唤醒”执行判断的后二部分:

新[头节点]存在 - 判断条件四不通过
新[头节点][等待状态]为0 - 判断条件五不通过

所有判断条件都不通过,不执行“状态唤醒”
T094 --> 3获取[状态]成功且不执行“状态唤醒”发现“状态唤醒”前/后的[头节点]不一致,循环/补充执行“状态唤醒”因为竞争获取[状态]失败而加入同步队列,共享节点C成为新共享首个非空节点
T103 --> 2获取[状态]成功且不执行“状态唤醒”发现“状态唤醒”前/后的[头节点]不一致,循环/补充执行“状态唤醒”再次因为竞争获取[状态]失败,将新[头节点][等待状态]由0CAS赋值为<信号>
T112 --> 1获取[状态]成功且不执行“状态唤醒”发现“状态唤醒”前/后的[头节点]不一致,循环/补充执行“状态唤醒”再次因为竞争获取[状态]失败,进入有限/无限等待状态

 判断条件六:s == null & 判断条件七:s.isShared()
 
    判断条件六/七用于判断首个非空节点[线程]是否存在及是否为共享模式。上文中已经说过,由于节点入队流程相关行为的顺序原因,[后继节点]为null不表示真的没有后继节点,因此当[后继节点]为null时共享节点[线程]也要继续执行“状态唤醒”以维护“共享节点[线程]状态唤醒的传播性”。由于“共享节点[线程]状态唤醒的传播性”只需唤醒共享首个非空节点[线程],因此当首个非空节点为独占节点时“状态唤醒”不会执行。而判断条件六则相对特殊的原因是因为在该情况下是无法确定后继节点的[模式/后继等待者]的。

    基于“共享节点[线程]状态唤醒的传播性”而被唤醒的未必是共享首个非空节点[线程]。这其实是很容易想到的,因为当共享节点的[后继节点]为null时,首个非空节点的[模式/后继等待者]将无法被确定,因此只能强制执行“状态唤醒”以保证传播。此外即使共享节点的[后继节点]被确定为共享节点,但其同时也可能是取消节点,因此根据查找规则实际的首个非空节点也完全可能是独占节点。

    在上文中我们讲述过一个“取消/状态唤醒”并发而导致“状态唤醒”无法查找/唤醒首个非空节点[线程]的“[头节点]错误信号”场景,而当时我们虽然给出了具体的场景和方案,但因为尚未讲解“状态唤醒”而未给出方案的具体含义,故而在文章的最后我们再进行详细解释。上文中我们其实已经阐明了“[头节点]错误信号”场景发生的核心原因,即无法在并发环境下保证取消CAS必然位于唤醒CAS的上/中游,故而只要能够保证/弥补这一点就能够避免/修补“[头节点]错误信号”场景,而令取消节点[线程]在取消CAS成功后判断“真前驱节点的[线程]是否不为null”就为是否弥补提供了判断依据。

    真前驱节点的[线程]为null代表着两种情况:一是真前驱节点已被取消;二是成为[头节点]。而在后者的情况中,由于执行“状态唤醒”的独占/共享线程对首个非空节点[线程]的查找/唤醒必然位于真前驱节点成为[头节点]/空节点/虚拟节点的下游,因此取消节点[线程]一旦发现真前驱节点的[线程]为null就意味着“可能”产生了“[头节点]错误信号”场景,故而此时就可以执行“清理唤醒”来修补该场景。而即使此时“[头节点]错误信号”场景并未发生或真前驱节点实际是被取消也无妨,因为前者只会造成重复唤醒;而后者只会造成无效唤醒,两者都不会导致程序安全性被破坏…相关场景如下:

取消节点[线程]独占真前驱节点[线程]共享真前驱节点[线程](释放时执行)共享真前驱节点[线程](获取时执行)
T01判断取消节点位于同步队列的内部,即真前驱节点不为[头节点]
T02判断真前驱节点的[等待状态]不为<信号>且 <= 0
T03成功获取[状态]并令真前驱节点成为[头节点]成功获取[状态]并令真前驱节点成为[头节点]成功获取[状态]并令真前驱节点成为[头节点]
T04不满足维护条件,不执行“状态唤醒”满足维护条件,执行“状态唤醒”,发现真前驱节点/[头节点]的[等待状态]为0,将之CAS赋值为<传播>, 不唤醒真后继节点/首个非空节点[线程]
T05断开与真前驱节点/[头节点]的引用使之成为空/虚拟节点断开与真前驱节点/[头节点]的引用使之成为空/虚拟节点
T06发现真前驱节点/[头节点]的[等待状态]为0, 不唤醒真后继节点/首个非空节点[线程]发现真前驱节点/[头节点]的[等待状态]为0,将之CAS赋值为<传播>, 不唤醒真后继节点/首个非空节点[线程]
T07将真前驱节点/[头节点]的[等待状态]CAS赋值为<信号>
T08判断真前驱节点的[线程]是否不为null,否则执行“清理唤醒”

 
 

条件


    所谓条件具体是指AQS类实现于Condition @ 条件接口的内部类ConditionObject @ 条件对象,是AQS类核心内容的最后部分。条件接口被Java设计用来定义条件机制,条件机制的本质是线程管理机制,用于在并发环境下实现对线程的有序协调及精确控制,即令线程有序的等待/唤醒。条件对象类是Java JDK对条件接口的唯一实现,用于为已同步的独占线程提供暂时解除同步的能力。该知识点相对独立且十分庞大,大致有上文四分之一的体量,因此会在相应文章中单独讲述,文章可在前言的涉及内容中进行跳转。

标签:状态,非空,Java,AQS,Lock,线程,共享,唤醒,节点
From: https://blog.csdn.net/qq_39288456/article/details/143307889

相关文章

  • JavaScript基础知识——黑马JavaWeb学习笔记
    JavaScript基础JavaScript:跨平台、面向对象的脚本语言(脚本语言:不需要编译,浏览器解释完直接运行)作用:控制网页行为,使网页可交互ps:JavaScript与Java是两门完全不同的语言本文为学习黑马程序员JavaWeb开发教程中JS部分学习笔记文章目录JavaScript基础一、JS引入方式1.......
  • 基于Java+SpringBoot+Mysql实现的古诗词平台功能设计与实现九
    一、前言介绍:1.1项目摘要随着信息技术的迅猛发展和数字化时代的到来,传统文化与现代科技的融合已成为一种趋势。古诗词作为中华民族的文化瑰宝,具有深厚的历史底蕴和独特的艺术魅力。然而,在现代社会中,由于生活节奏的加快和信息获取方式的多样化,古诗词的传播和阅读面临着一......
  • 基于Java+SpringBoot+Mysql实现的古诗词平台功能设计与实现十
    一、前言介绍:1.1项目摘要随着信息技术的迅猛发展和数字化时代的到来,传统文化与现代科技的融合已成为一种趋势。古诗词作为中华民族的文化瑰宝,具有深厚的历史底蕴和独特的艺术魅力。然而,在现代社会中,由于生活节奏的加快和信息获取方式的多样化,古诗词的传播和阅读面临着一......
  • Java Z 垃圾收集器如何彻底改变内存管理
    大家好,我是V哥,今天的内容来聊一聊ZGC,JavaZGarbageCollector(ZGC)是一个低延迟垃圾收集器,旨在优化内存管理,主要用于大内存应用场景。它通过以下几个关键创新,彻底改变了传统Java的内存管理方式:V哥总结的以下5点,欢迎一起讨论。1.极低的暂停时间ZGC的暂停时间一般保持在10毫......
  • 前端JavaScript的异步编程:从回调到Promise再到Async/Await
    写在前面在前端开发中,异步编程是一个非常重要的概念。随着JavaScript语言和前端技术的发展,异步编程的模式也在不断演进。本文将带你了解从最初的回调函数(Callback)到Promise,再到现代的Async/Await,这些异步编程模式的演变过程。回调函数(Callback)回调函数是最早期的异步编程......
  • JAVA基础必备集合框架 @简单易懂
    Java基础框架是指在Java开发中常用的一些框架和库,它们提供了一些通用的功能,以简化开发过程。以下是一些重要的Java基础框架的详细讲解:1.SpringFramework概述Spring是一个广泛使用的企业级应用开发框架,提供了全面的基础设施支持,特别适合构建JavaEE应用。主要特性控制反......
  • java计算机毕业设计在线票务系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着互联网技术的飞速发展,在线票务系统已经成为现代生活中不可或缺的一部分。传统的售票方式面临着排队等候、购票速度慢、安全性差等问题,而在线票务......
  • java计算机毕业设计员工管理系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景、意义和目的随着信息技术的迅猛发展,企业管理和运营的方式也在不断变革。传统的手工管理模式已经无法满足现代企业对效率、准确性和实时性的要求。特......
  • Java毕业设计-基于Springboot框架的文学创作类社交论坛系统项目实战(附源码+论文)
    大家好!我是岛上程序猿,感谢您阅读本文,欢迎一键三连哦。......
  • Java毕业设计-基于Springboot框架的学生信息管理系统项目实战(附源码+论文)
    大家好!我是岛上程序猿,感谢您阅读本文,欢迎一键三连哦。......