前言
相关系列
- 《Java & Lock & 目录》(持续更新)
- 《Java & Lock & ConditionObject & 源码》(学习过程/多有漏误/仅作参考/不再更新)
- 《Java & Lock & ConditionObject & 总结》(学习总结/最新最准/持续更新)
- 《Java & Lock & ConditionObject & 问题》(学习解答/持续更新)
涉及内容
- 《Java & Lock & 总结》
- 《Java & Lock & Condition & 总结》
- 《Java & Lock & LockSupport & 总结》
概述
简介
ConditionObject @ 条件对象类用于管理AQS访问线程。条件对象类是AQS类对条件接口的内部实现,同时也是Java JDK对条件接口的唯一实现。条件接口被Java设计用来定义条件机制,条件机制的本质是线程管理机制,用于在并发环境下实现对线程的有序协调及精确控制,即令线程有序的等待/唤醒。AQS类通过对条件机制的引入/实现对外提供了一套可控制自身访问线程的基础手段,使得开发者可以根据不同需求/环境/规则对AQS访问线程进行逻辑控制。
/*** Condition implementation for a {@link AbstractQueuedSynchronizer} serving as the basis of a {@link Lock}* implementation.* AQS的条件实现用于充当锁实现的基础。* <p>* Method documentation for this class describes mechanics, not behavioral specifications from the point of view of* Lock and Condition users. Exported versions of this class will in general need to be accompanied by documentation* describing condition semantics that rely on those of the associated {@code AbstractQueuedSynchronizer}.* 该类的方法文档从锁和条件用户的角度描述了机制,而不是行为规范。这个类的导出版本通常需要附带描述依赖于相* 关AbstractQueuedSynchronizer的条件语义的文档。* <p>* This class is Serializable, but all fields are transient, so deserialized conditions have no waiters.* 该类是序列化的,但所有字段被修饰了transient,所以反序列化的条件没有等待者。*/
public class ConditionObject implements Condition, Serializable {...
}
条件对象类用于对暂时失去同步意愿的线程进行管理。我们可能会对“条件对象类用于管理AQS访问线程”这一说法产生疑惑,因为AQS类本身就是一套协调/控制线程的线程管理机制,为什么还要在其基础上额外引入条件机制呢?这是因为AQS类专用于对有同步意愿的线程进行管理,即其只能管理想要/已被同步的线程。但实际情况是线程可能在某些情况下暂时失去同步意愿,但又因为会被恢复而继续对AQS保持访问状态,从而使得AQS类无法管理此类线程,因此才需要引入/实现条件机制对之进行管理。
线程暂时失去同步意愿的实际场景有且仅有暂时解除同步一种,相应场景如下:
具有同步意愿:线程一/多次通过AQS达成同步/获取[state @ 状态];
失去同步意愿:线程在同步后执行逻辑,后因条件未达成而暂停并暂时解除同步/彻底释放[状态],随后被安排进入条件对象中等待恢复同步;
恢复同步意愿:线程因为条件达成而被唤醒并恢复同步/一次性获取释放的所有[状态],随后从逻辑暂停处继续执行逻辑。
条件对象类只支持对独占线程进行管理。通过上文所述场景我们可知线程为了等待某些条件达成会暂时解除同步,但与此同时我们可能产生新的疑惑:等待条件达成与暂时解除同步之间存在何种联系?为什么线程要在等待的过程中暂时解除同步呢?这是因为条件的达成可能需要由其它线程在同步时实现,因此如果在该情况下独占线程不暂时解除同步,则其将因为条件无法达成而永久等待。暂时解除同步后的独占线程会交由条件对象负责管理(等待/唤醒)。那为什么说条件对象类只支持对独占线程进行管理呢?这是因为对于共享线程来说,由于设计上允许其它共享线程并行同步的原因,条件完全可以在不解除同步的情况下达成,因此条件对象类不支持也不需要对共享线程进行管理。
条件对象类是非静态的公共类。条件对象类是公共类意味着开发者可以通过常规的new关键字创建条件对象,而非静态则是一个需要特别注意的点,因为这意味着条件对象并非与AQS类关联而是与具体的AQS关联,而这就侧面表示了条件对象类对独占线程的管理是分批而非统一的,即每个条件对象只能管理自身所属AQS的独占线程而无法对其它AQS的独占线程进行管理。
方法
等待
-
public final void await() throws InterruptedException —— 等待 —— 令已独占同步当前AQS的当前线程原子性地解除同步并在当前条件对象中进入等待状态。该方法是等待方法“阻塞”形式的实现,当前线程会无限等待至因为信号而唤醒为止并重新恢复对当前AQS的独占同步至初始状态。当前线程在进入方法/等待恢复期间如果已/被中断会抛出中断异常,但中断状态会被清除。
-
public final void awaitUninterruptibly() —— 等待(不可中断) —— 令已独占同步当前AQS的当前线程原子性地解除同步并在当前条件对象中进入等待状态。该方法是等待方法“阻塞不可中断”形式的实现,当前线程会无限等待至因为信号而唤醒为止并重新恢复对当前AQS的独占同步至初始状态。当前线程在进入方法/等待恢复期间如果已/被中断不会抛出中断异常,但中断状态会被保留。
-
public final boolean await(long time, TimeUnit unit) throws InterruptedException —— 等待 —— 令已独占同步当前AQS的当前线程原子性地解除同步并在当前条件对象中进入等待状态。该方法是等待方法“超时”形式的实现之一,当前线程会有限等待至因为信号而唤醒为止并在重新恢复对当前AQS的独占同步至初始状态后返回true,超时指定等待时间则返回false。当前线程在进入方法/等待恢复期间如果已/被中断会抛出中断异常,但中断状态会被清除。
-
public final long awaitNanos(long nanosTimeout) throws InterruptedException —— 等待纳秒 —— 令已独占同步当前AQS的当前线程原子性地解除同步并在当前条件对象中进入等待状态。该方法是等待方法“超时”形式的实现之一,当前线程会有限等待至因为信号而唤醒为止并在重新恢复对当前AQS的独占同步至初始状态后返回剩余等待纳秒,超时指定等待时间则返回0或负数。当前线程在进入方法/等待恢复期间如果已/被中断会抛出中断异常,但中断状态会被清除。
-
public final boolean awaitUntil(Date deadline) throws InterruptedException —— 等待 —— 令已独占同步当前AQS的当前线程原子性地解除同步并在当前条件对象中进入等待状态。该方法是等待方法“超时”形式的实现之一,当前线程会有限等待至因为信号而唤醒为止并在重新恢复对当前AQS的独占同步至初始状态后返回true,超时指定等待时间则返回false。当前线程在进入方法/等待恢复期间如果已/被中断会抛出中断异常,但中断状态会被清除。
信号
-
public void signal() —— 信号 —— 唤醒一条在当前条件对象中等待的线程以令之重新恢复对当前AQS的独占持有。
-
public void signalAll() —— 信号全部 —— 唤醒所有在当前条件对象中等待的线程以令之重新恢复对当前AQS的独占持有。
实现
条件队列
条件对象类采用条件队列管理暂时解除同步的独占线程。出于公平唤醒及共用节点类等原因,条件对象类采用与AQS类相似的队列结构来管理独占线程,该队列被称为条件队列。条件队列与同步队列一样都是逻辑队列,条件对象类使用[firstWaiter/lastWaiter @ 首个等待者/最后等待者]来持有条件队列头/尾独占节点的引用,从而变相持有整个条件队列。虽说与同步队列共用节点类,但条件队列并不使用[前驱/后继节点]链接独占节点:一是因为条件队列是单向链表,独占节点并不需要持有前驱引用;二是因为独占节点存在同时位于同步/条件队列的情况,因此为了支持该情况也必须使用不同的字段来持有其在两类队列中的后继引用。该知识点在AQS类的相应文章中已经提及过,此处再进行详细阐述:独占节点使用[nextWaiter @ 模式/后继等待者]持有其在条件队列中的后继引用,而由于只有独占节点才可能加入条件队列,因此该使用方式并不会与[模式/后继等待者]原本表示模式的功能设计产生冲突。
独占节点的[模式/后继等待者]未必一定为<EXCLUSIVE @ 独占 @ null>。AQS类的文章在讲解节点时说过AQS类使用<独占>和<SHARED @ 独占 @ node>两个静态常量节点作为表示独占/共享模式的标记,因此当[模式/后继等待者]为<独占>时便意味着其为独占节点…但实际上该说法并不准确…因为就在刚才我们提到了独占节点存在同时位于同步/条件队列的情况,而又因为独占节点使用[模式/后继等待者]持有其在条件队列中的后继引用,因此[模式/后继等待者]也完全可以在同步队列中持有具体引用。那能否说[模式/后继等待者]为<独占>就表示独占节点只位于同步队列中呢?也不可以…因为当独占节点恰好为条件队列的尾节点时其[模式/后继等待者]也一样为<独占>,因此想要准确判断节点是否为独占模式实际上只能通过查看[模式/后继等待者]是否不为<共享>判断。
一个AQS可能关联多个条件队列。与同步队列只有一个不同,一个AQS可能关联了多个条件队列。这并不是说条件队列可以在条件对象中存在多个,而是AQS类允许基于一个AQS创建多个条件对象。该特性使得开发者可以进一步细化对独占线程的管理,即即使是同属于一个AQS的独占线程也可以通过调用不同条件对象来进行分批管理,只要条件对象基于该AQS创建即可。
等待/暂时解除同步
条件对象类将线程同步的暂时解除与等待进行了合并。AQS类通过条件对象类的等待方法令独占线程暂时解除同步,即当独占线程调用条件对象的等待方法时会同时完成暂时解除同步及等待两项功能。因此条件对象类不单单只是负责对独占线程的管理,还是令独占线程暂时解除同步的操作入口。
独占线程会自封为独占节点尾插至条件队列中。当独占线程调用等待方法暂时解除同步时,其会将自身封装为[waitStatus @ 等待状态]为<CONDITION @ -2 @ 条件>的独占节点并尾插至条件队列中。整个尾插过程并不需要CAS的保护,因为此时的独占线程尚未解除同步,故而在此期间不会有其它独占线程参与竞争而导致线程安全问题。那是否存在独占节点直接从同步队列迁移至条件队列的情况呢?这个问题确实值得思考,因为独占线程在其位于同步队列等待同步的过程中暂时失去同步意愿似乎也是有可能的…但很遗憾这种情况并不存在。上文已经说过,目前关于独占线程暂时失去同步意愿的具体场景只存在暂时解除同步一种,而对于已同步的独占线程来说即使其曾经确实被安排加入过同步队列以等待同步,也会在成功同步后断开与其所属独占节点的关联。因此即使其所属独占节点依旧以[head @ 头节点]的形式存在于同步队列中,独占线程也必然已从同步队列中脱离,故而加入条件队列的独占节点必然是全新创建的。
独占线程可能在加入条件队列前触发对条件队列的清理。在将自身封装为独占节点并加入条件队列之前,独占线程会先判断尾节点是否存在且是否为取消节点,是则触发对条件队列的清理。该知识点会在下文讲解取消时详述,此处之所以会特别提及源于该触发时机属于“弥补”性质,即专用于处理执行清理的独占线程在某些情况下无法判断独占节点是否位于条件队列中的情况。
独占线程会在成功加入条件队列后暂时解除同步。暂时解除同步的本质是彻底释放所有[状态]并唤醒同步队列的首个非空节点[线程],这里强调彻底是因为独占线程可能因为重入而经历过多次同步。但无论其达成同步/获取[状态]多少次在暂时解除同步时都会被一次性释放,以使得其它独占线程获得被同步的可能。暂时解除同步可能失败,原因是其底层调用的tryRelease(int arg)方法由AQS类子类负责实现,因此无法保证[状态]彻底释放的必然成功。条件队列中独占线程会在暂时解除同步失败(理论上不允许失败)时抛出非法监视器状态异常,但独占节点则会在[等待状态]会被赋值为<CANCELLED @ 1 @ 取消>后继续保留在条件队列中等待被清理,这也是独占节点在条件队列中唯一会被显式取消的情况。
独占线程会在暂时解除同步后进入有限/无限等待状态。独占线程进入有限/无限等待状态的本质是LockSupport.park(…)方法的自我调用,该内容的关键在于无论何种形式的等待方法都必须以循环的方式调用LockSupport.park(…)方法以满足“实现需求设计”与“避免虚假唤醒”两方面的要求。以“实现需求设计”举例:对于不可中断形式的等待方法而言,如果独占线程被中断唤醒,则独占线程可以通过循环再次进入有限/无限等待状态以避免在自身条件未达成时恢复同步。条件对象类将“独占节点不位于同步队列中”作为等待循环的判断条件,原因是将独占节点从条件队列迁移至同步队列只会在符合需求设计的唤醒前/后发生,例如条件达成时的信号唤醒前;又例如可中断/定时的等待方法中发生的中断/超时唤醒后等,因此独占节点位于同步队列中便意味着独占线程已无需/不许再继续等待,从而退出等待循环。
“避免虚假唤醒”也是等待必须循环执行的必要理由。虚假唤醒是一种发生于LockSupport.park(…)方法的奇妙现象,具体表现为因LockSupport.park(…)方法而进入有限/无限等待状态的线程可能在毫无理由的情况下被唤醒。虚假唤醒并不属于符合需求设计的唤醒,因此其发生前/后都不应该迁移独占节点,故而如果不辅以循环,独占线程将可能在不符合业务的情况下提前结束等待,而这显然是不允许的。事实上由于虚假唤醒这一奇妙现象的存在,将LockSupport.park(…)方法置于相应逻辑的循环中执行才是官方指定的正确调用方式。
判断独占节点是否位于同步队列中并不十分耗时。条件对象类通过调用AQS类的isOnSyncQueue(Node node)方法判断独占节点是否位于同步队列中,而其操作本质是以[尾节点]/独占节点为起/终点的前遍历查找行为。当独占节点已迁移至同步队列中时,由于同步队列是尾插法,又因为迁移与唤醒(无论两者前后顺序如何)是紧密相连的操作/时间间隔非常短,因此在独占线程前遍历查找时独占节点通常就位于同步队列的尾部附近,故而并不十分耗时。那如果独占节点尚未被迁移至同步队列中呢?岂不是要进行全遍历?这难道还不耗时么?的确按正常逻辑来说如果在同步队列中等待同步的线程非常多的话那确实将是一项非常耗时的操作…但这并非是无法弥补的。AQS类采用了多项优化方案避免独占线程在查找所属独占节点时全遍历同步队列,这些优化方案可以对独占节点位于/不位于同步队列的大部分情况进行快速筛选,从而使得全遍历行为只在少数极端条件下执行…优化策略具体如下:
判断独占节点的[等待状态]是否为<条件> —— <条件>是独占节点"只"位于条件队列中时才可能存在的状态,当独占节点被迁移至同步队列时其[等待状态]会被赋值为0,因此如果发现独占节点的[等待状态]为<条件>便意味着其必然不位于同步队列中;
判断独占节点的[前驱节点]是否为null —— 独占节点的[前驱节点]为null意味着独占节点不位于或以[头节点]的形式存在于同步队列中。由于此时的独占线程尚未恢复同步,因此其所属独占节点不可能成为[头节点],因此如果独占节点的[前驱节点]为null便意味着其必然不位于同步队列中;
判断独占节点的[后继节点]是否不为null —— 独占节点的[后继节点]为null并不代表其一定不位于同步队列中,因为同步队列[尾节点][后继节点]也为null,但如果独占节点的[后继节点]不为null便意味着其必然位于同步队列中。
信号唤醒
在条件队列中有限/无限等待的独占线程会因为信号/中断/超时/虚假四种原因被唤醒。我们已知虚假唤醒是需求设计所不允许的特殊情况,并也已通过等待循环的方式弥补,因此关于唤醒的核心内容会集中在其它三种唤醒操作上。事实上这三种唤醒操作单独讲解起来都并不复杂,但关键在于三者是允许交错并发的,这就要求条件对象类必须能够在复杂的并发场景中准确查明/分辨出独占线程被唤醒的真实原因以对外作出正确反馈,而这才是唤醒操作的真正难点所在。事实上想要判断得知独占线程被唤醒的真实原因是难以/无法做到的,原因是缺乏可靠的判断依据,因此条件对象类采用“标记”方案定性独占线程的唤醒方式。“标记”方案的核心为抢占,即当多种唤醒操作并发时独占线程会被定性为被优先标记的唤醒方式唤醒,因此独占线程的实际唤醒方式与标记唤醒方式可能并不相同,该知识点会在下文讲解三种唤醒操作时详述。
“信号唤醒”只能由正在同步的独占线程执行。成功同步的独占线程可以通过调用信号方法中的signal()方法唤醒在条件队列中等待的首个独占节点[thread @ 线程],此外其还可以调用signalAll()方法唤醒在条件队列中等待的所有独占线程。signalAll()方法会遍历条件队列并依次唤醒所有的独占线程,这在效果上与循环调用signal()方法是等价的,但避免了一些非必要的操作。
“信号唤醒”可能因为失败而重复执行。“信号唤醒”失败的原因是因为独占线程已被并发标记为中断/超时唤醒,由于唤醒方法存在必须令等待中的独占线程会被信号唤醒的定义,因此失败时signal()方法会重新定位新首个独占线程以再次执行“信号唤醒”,直至唤醒成功或无首个独占节点[线程]可被唤醒为止。
“信号唤醒”会尝试将独占节点的[等待状态]由<条件>CAS赋值为0,该CAS被称为标记CAS。标记CAS并非只是单纯的CAS赋值,而是所有唤醒操作都必须执行的关键步骤,其作用具体有四:一是将[等待状态]还原为初始状态0以备独占节点加入同步队列;二是将独占线程的唤醒方式标记为自身;三是判断独占节点是否已被取消;四是判断独占线程是否已被标记为被其它唤醒方式唤醒。
独占节点最初会以<条件>的[等待状态]加入条件队列,又因为[等待状态]会在独占线程暂时解除同步失败时被显式赋值为<取消>,因此标记CAS失败就意味着独占节点可能已被取消;此外由于所有的唤醒都会执行标记CAS,因此失败还意味着独占线程可能已被标记为被其它唤醒方式唤醒;最后如果标记CAS成功,则意味着独占线程已被标记被当前唤醒方式唤醒,因为其它唤醒操作的标记CAS会因为当前唤醒操作的标记CAS成功而失败。但请注意!独占线程被标记为指定的唤醒只是近似的模拟,是因为无法准确探知而选取的最大可能,并不意味着独占线程实际就是被标记的唤醒方式所唤醒…例如下表中就展示了独占线程被中断/超时唤醒但又被标记为信号唤醒的情况:
信号唤醒 | 中断/超时唤醒 | |
---|---|---|
T01 | 独占线程因为中断/超时而唤醒 | |
T02 | 将独占节点[等待状态]由<条件>CAS赋值为0成功,独占线程被标记为信号唤醒 | |
T03 | 将独占节点[等待状态]由<条件>CAS赋值为0失败,认定独占线程已被信号唤醒 |
被成功标记CAS的独占节点会被迁移至同步队列以重新竞争同步/恢复同步意愿。被标记为信号唤醒的独占线程会将所属独占节点从条件队列中迁移至同步队列,即独占节点会从条件队列中头部移除并尾插至同步队列中。独占节点被迁移至同步队列意味着独占线程重新参与了对同步的竞争,并由此宣告其正式恢复同步意愿。需要重点提及是:由于独占节点在加入同步队列前会从条件队列的头部移除,即会将[模式/后继等待者]置null以断开与条件队列的关联,因此“信号唤醒”中被迁移的独占节点只存在于同步队列中。但除此以外,被迁移的独占节点还有同时存在于同步/条件队列的情况,该知识点会在下文讲解中断/超时唤醒时详述。
“信号唤醒”的核心是“迁移”而非“唤醒”,其只保证独占线程必然会被唤醒,但不保证由执行“信号唤醒”的同步独占线程执行。独占节点被迁移至同步队列后,此时的独占线程依然处于有限/无限等待状态,并且未必会被立即信号唤醒,即“信号唤醒”并非一定会对独占线程执行LockSupport.unpark(Thread thread)方法。之所以会如此是因为独占节点加入同步队列后独占线程为了等待同步也需要进入有限/无限等待状态,因此“信号唤醒”通常不需要将其唤醒,只需放任其等待“状态唤醒”即可。当然情况也并非完全如此,在两种情况下“信号唤醒”也需要唤醒独占线程:一是独占节点的[前驱节点]为取消节点,这是出于令独占线程执行“等待 - 前驱式清理”的目的;二是在“状态唤醒”执行线程将独占节点[前驱节点]的[等待状态]由0CAS赋值为<SIGNAL @ -1 @ 信号>失败时,因为这可能导致独占线程永远无法被唤醒,是与取消CAS相同性质的行为,因此需要将之信号唤醒以执行“等待 - 等待CAS”操作。
中断/超时唤醒
被唤醒的独占线程会第一时间判断自身是否已被中断。无论实际唤醒方式及等待方法的形式为何,独占线程被唤醒后都会第一时间通过响应中断的方式判断自身是否已被中断,这么做的目的是确定自身是否具有被中断唤醒的可能,并以此为依据执行“中断唤醒”,故而“中断唤醒”的执行者实际上是被唤醒的独占线程本身。
“中断唤醒”会执行标记CAS。当标记CAS成功时,被标记为中断唤醒的独占线程会迁移独占节点至同步队列中以重新竞争同步/恢复同步意愿,因此与“信号唤醒”不同,“中断唤醒”中的独占线程是以唤醒的形态被迁移至同步队列的,并且迁移也位于独占线程被唤醒之后。此外需要特别注意的是:“中断唤醒”在迁移独占节点时并不会将其从条件队列中移除,原因是该独占节点未必与“信号唤醒”中的独占节点一样是条件队列的头节点,即不一定是[首个等待者],因此想要移除就必须先对条件队列进行遍历查找,而这显然是不高效也不安全的行为。因为这即可能导致频繁的长遍历,又可能因为多独占线程并发中断迁移而导致部分独占节点被遗留在条件队列中…因此也就直接导致了独占节点同时位于同步/条件队列情况的产生。
“中断唤醒”的标记CAS失败意味着独占线程已被并发标记为信号唤醒,在这种实际被中断唤醒但又被标记为信号唤醒的情况下独占线程会额外判断/保证独占节点在自身退出等待循环前必然位于同步队列中。之所以要进行额外判断/保证是因为独占线程会在确认自身被中断后不经下次的等待判断而直接退出当前等待循环,而又因为“信号唤醒”对独占节点的迁移位于标记CAS之后,因此此时的独占节点[等待状态]虽然已被CAS赋值为0,但可能尚未被迁移至同步队列中。而由于独占线程正处于唤醒状态,因此当其退出等待循环并尝试获取[状态]时将可能因为独占节点不位于同步队列/没有[前驱节点]而抛出NullPointerException @ 空指针异常…相关源码及模拟异常场景如下:
while (!isOnSyncQueue(node)) {LockSupport.park(this);// ---- 独占线程在确定自身被中断后会直接退出等到循环,因此checkInterruptWhileWaiting方法中包含了响应中断和迁移保障的逻辑。if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;
}
信号唤醒/同步线程 | 中断/独占线程 | |
---|---|---|
T01 | 因为中断而被唤醒,并发现自身被中断 | |
T02 | 执行标记CAS成功 | 执行标记CAS失败 |
T03 | 直接退出等待循环 | |
T04 | 尝试获取[状态]并因为独占节点不位于同步队列/没有[前驱节点]而抛出空指针异常 | |
T05 | 将独占节点从条件队列迁移至同步队列 |
独占线程通过yield()方法保证所属独占节点在其退出等待循环前必然位于同步队列中。当独占线程在额外判断中发现独占节点未位于同步队列时会短暂放弃CPU资源以期望自身再次获取CPU资源时独占节点已加入同步队列,由于该行为无法保证必然性,因此yield()方法同样需要循环执行。
while (!isOnSyncQueue(node))Thread.yield();
在定时形式的等待方法中如果独占线程被唤醒后发现自身没有被标记为中断/信号唤醒,则会在下个循环中判断自身是否有被超时唤醒的可能。独占线程通过查看是否存在剩余等待时间判断其是否可能被超时唤醒,这种可能在剩余等待时间不存在时成立,因此该情况下独占线程会执行“超时唤醒”…关键源码如下:
while (!isOnSyncQueue(node)) {// ---- 判断是否已经超时,超时的优先级比等待和中断更高,因此被超时唤醒的独占线程只// 能在下次循环中判断超时。而在这个过程中独占线程可能被标记为信号/中断唤醒。if (nanosTimeout <= 0L) {timedout = transferAfterCancelledWait(node);break;}// ---- 在等待时间剩余较多的情况下等待,否则直接自旋。if (nanosTimeout >= spinForTimeoutThreshold)LockSupport.parkNanos(this, nanosTimeout);// ---- 独占线程在确定自身被中断后会直接退出等到循环。if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;nanosTimeout = deadline - System.nanoTime();
}
“超时唤醒”与“中断唤醒”在流程上是相同的。“超时”及“中断”两种唤醒操作底层都调用transferAfterCancelledWait(Node node)方法实现,该方法用于执行标记CAS,并在成功后将独占节点迁移至同步队列,或在失败后判断/保证独占线程在退出等待循环前令独占节点位于同步队列中。两者的区别在于“超时唤醒”会直接调用该方法,而“中断唤醒”则会在checkInterruptWhileWaiting(Node node)方法中确认独占线程被中断后再间接调用该方法。从上述源码中我们可以发现由于“超时唤醒”与“中断唤醒”会先后执行,因此对于一个独占线程而言虽然中断/超时唤醒之间确实可能并发,但两套操作却不存在并发情况。而对于“信号唤醒”而言其虽然知道标记CAS失败是因为“中断/超时唤醒”的原因,但却不知道也没必要知道具体是两者中的哪一个所导致的。
恢复同步
被唤醒的独占线程会恢复同步意愿。我们已知“信号/中断/超时唤醒”会将独占节点迁移至同步队列中以令之重新竞争同步,由此宣告独占线程正式恢复同步意愿。重新竞争同步与常规竞争同步最大的区别在于其会一次性获取之前释放的所有[状态],从而将独占线程的同步恢复到与暂时解除同步前完全一致的情况。此外重新竞争同步不会中断/超时,无论独占线程最初是否调用了可中断/定时形式的获取方法来获取同步,在重新竞争时都不会抛出中断异常或放弃同步,因为中断/超时只作用于独占线程首次达成同步的时候,该设计保证了独占线程的同步必然可被恢复。
被标记为中断唤醒的独占线程会在恢复同步后抛出中断异常。当独占线程再次经历竞争而恢复同步后,如果其被标记为中断唤醒,并且等待方法的形式允许中断,则其会直接抛出中断异常。一个值得思考的问题是:为什么独占线程不直接在唤醒时抛出中断异常而是在恢复同步后再抛出呢?实际上这是为了保证独占线程同步的必然恢复,以维护独占线程对[状态]的获取/释放必须成对出现的使用设定。事实上对于抛出中断异常该行为可中断形式的等待方法只允许两种情况:一是在独占线程尚未暂时解除同步时发生;二是在独占线程恢复同步后发生。该两者的核心都在于必须保证中断异常抛出时独占线程处于同步状态。
被中断但未被标记为中断唤醒的独占线程不会抛出中断异常,但中断状态会被保留。在独占线程被实际中断唤醒但未被标记中断唤醒的情况下,无论并发场景为何,中断都会被统一视作在独占线程被唤醒后产生。而由于独占线程未被标记为中断唤醒,因此该情况在需求设计上不满足中断异常的抛出条件,只会保留中断状态以表示独占线程遭遇过中断。此外如果等待方法的形式不允许中断,则即使独占线程被标记为中断唤醒也只会保留中断状态而不会抛出中断异常。需要注意的是:被保留的中断状态属于重中断,是等待方法为了保留中断状态而额外执行的中断,原因是独占线程无论是在暂时解除同步后的条件队列中被中断还是在恢复同步后的同步队列中被中断其中断状态都会因为中断判断而被响应消除。为此等待方法会记录/整合两个阶段的中断判断结果为以下三种标记,以作为其选择中断处理方案的依据:
<REINTERRUPT @ 1 @ 重中断>:表示独占线程在条件队列被中断但未被标记为中断唤醒,以及在同步队列中被中断的标记,该标记意味着等待方法在退出时需要重中断以保留中断状态,因为独占线程原本的中断状态会因为中断判断而被响应消除;
<0>:表示独占线程未被中断的标识,该标记意味着等待方法在退出时即不需要抛出中断异常也不需要重中断以保留中断状态;
<THROW_IE @ -1 @ 抛出异常>:表示独占线程条件队列中被标记中断唤醒的标记,该标记意味着等待方法在退出时需要抛出中断异常。
取消
条件队列中所有[等待状态]不为<条件>的独占节点都属于取消节点。与同步队列不同,除[等待状态]被显式赋值为<取消>的独占节点属于取消节点外,条件队列中其它[等待状态]不为<条件>的独占节点也都会被视作隐式取消节点。由此可知被标记为超时/中断唤醒的独占节点本质上都属于隐式取消节点,因为这些独占节点在标记CAS后会直接加入同步队列且不会从条件队列中移除。而被标记为信号唤醒的独占节点由于会真正意义上的“迁移”因此不属于取消节点的范围…虽然其也会因为标记CAS和迁移之间存在时间间隔而短暂同时位于同步/条件队列中,但由于时间过于短暂因此通常不在意这一点。
隐式取消节点[线程]会全局清理条件队列中的取消节点。隐式取消节点[线程]会在其恢复同步后,抛出中断异常/重中断之前遍历性的全局清理条件队列中的取消节点,之所以只有隐式取消节点[线程]会在该时段执行清理有两点原因:一是因为显式取消节点[线程]会直接抛出非法监视器状态异常而无法执行;二是如果令显式取消节点[线程]也在此时执行清理将导致大量的无用功。因为取消节点通常与隐式取消节点在数量上等同,因此令隐式取消节点[线程]执行清理就已足够/过量满足清理所需,无需再令显式取消节点[线程]也执行唤醒。可以发现的是我们在进行数量对比时并没有考虑显式取消节点,因为只要符合调用规范,暂时解除同步理论上是不应该失败的,因此显式取消节点的产生本质上属于AQS类子类在实现上的缺陷,故而不纳入计算范围。
隐式取消节点[线程]通过判断[模式/后继等待者]是否为null决定自身是否执行清理。虽说上文在描述时一直使用“被标记为**唤醒的独占线程”这种说法,但实际上独占线程并无法明确得知自身被标记的唤醒方式,因为其并没有以字段的形式被保存下来。这大概率是出于节约内存方面的考量,因为该数据在整体流程中并无作为准确判断依据的必要,原因是隐式取消节点[线程]可以通过查看[模式/后继等待者]是否为null来代替判断自身是否被标记为中断/超时唤醒。隐式取消节点[线程]会因为其所属节点保留在条件队列中而[模式/后继等待者]不为null,因此[模式/后继等待者]不为null便可作为隐式取消节点[线程]触发清理的判断依据。但该判断方案并非是完美的,因为当取消节点为[最后等待者],即条件队列的尾节点时其[模式/后继等待者]也为null,因此在该情况下隐式取消节点[线程]对条件队列的清理将不会被触发。
独占线程可能在加入条件队列前触发对取消节点的清理。对于取消节点为[最后等待者]而无法触发清理问题的处理方案其实在上文中已经提及过了,即独占线程可能在加入条件队列前触发对条件队列的清理。独占线程在将自身封装为独占节点并尾插至条件队列前会先判断[最后等待者]是否为取消节点,是则先执行清理以弥补上文的情况。我们由此可知独占线程最多可能执行两次条件队列清理,并且暂时解除同步失败的线程也可能执行清理,因为其可以在独占节点入队前触发…但凭心而论该特殊处理显得没那么精妙,因为隐式取消节点[线程]完全可以在发现[模式/后继等待者]为null后再判断所属节点是否为[最后等待者]来执行清理,而无需在入队前判断来提升代码的复杂性…个人认为这可能是因为独占节点[线程]在入队时原本就需要获取[最后等待者]以作为其入队基础的原因,毕竟如此一来判断[最后等待者]是否为取消节点就成为了顺势行为,从而避免隐式取消节点[线程]专门执行这一点。
条件队列的清理是线程安全的,但无法保证取消节点全移除。无论是独占线程加入条件队列前/恢复同步后执行的清理都具有一个特点,即独占线程都处于同步状态。这意味着取消节点的清理是不会出现并发的,不会因为多独占线程并发执行而出现取消节点遗留的问题。但话说如此,取消节点遗留的现象是依然存在的,因为取消节点的产生是允许并发的,毕竟超时/中断唤醒并不会因为当前有独占线程持有同步而停止。