Java Lock CountDownLatch 总结
前言
相关系列
- 《Java & Lock & 目录》(持续更新)
- 《Java & Lock & CountDownLatch & 源码》(学习过程/多有漏误/仅作参考/不再更新)
- 《Java & Lock & CountDownLatch & 总结》(学习总结/最新最准/持续更新)
- 《Java & Lock & CountDownLatch & 问题》(学习解答/持续更新)
涉及内容
- 《Java & Lock & 总结》
- 《Java & Lock & CyclicBarrier & 总结》
概述
简介
CountDownLatch @ 倒数闭锁类是俗称“三剑客”的三类常用线程控制工具之一,用于通过批量拦截/释放确保指定数量的线程同时开始/结束对资源的访问。所谓拦截,本质是令线程进入等待状态。倒数闭锁类被广泛用于对多线程执行时机进行协调控制的场景,例如控制多线程任务同时执行/统一结束等。倒数闭锁类采用减法计数作为拦截线程总数的统计方式,其会在拦截线程总数到达拦截上限前拦截所有经过的线程,并在达到拦截上限时统一释放。从核心功能上来说,倒数闭锁类与“三剑客”中的CyclicBarrier @ 循环栅栏类是完全一致的,即都被设计用于对多线程任务进行批次控制。但两者在功能细节上却存在区别,最典型的差异是:倒数闭锁类无法像循环栅栏类一样实现多次拦截,拦截线程一旦被释放其便失去了作用,即后续线程可以随意通过倒数闭锁而不受限制。想要重新启用拦截功能只能实例化新倒数闭锁开启新流程,或者直接使用可多次拦截的循环栅栏类。此外,倒数闭锁类将线程的“拦截”与“计数”进行了拆分,使得拦截线程总数的递增与线程的拦截没有直接关系,即倒数闭锁类可以拦截与拦截上限不同数量的线程,并且线程释放也可以由拦截线程以外的线程控制,这显著增强了拦截的灵活性与可控性,该知识点会在下文讲解 “拦截”与“计数”的拆分时详述。
倒数闭锁类基于AQS类的共享模式实现。与基于AQS类独占模式实现的循环栅栏类不同,倒数闭锁类是基于AQS类的共享模式实现的,目的是借助其并发性增强拦截的性能。所谓基于AQS类实现,是指倒数闭锁类的相应方法实现本质都是对AQS类内部字段/方法/机制的赋值/重写/调用。但需要注意的是:倒数闭锁类并不是AQS类的子类,大多数基于AQS类实现的API都采用在内部定义/实现单/多个AQS类子类并调用实例的方式来实现自身设计,倒数闭锁类也不例外,该知识点会在下文讲解AQS时详述。
与循环栅栏类的对比
- 倒数闭锁类是一次性的,只能进行单次批量拦截;而循环栅栏类则支持多次批量拦截;
- 倒数闭锁类不支持在拦截线程释放时执行自定义操作;而循环栅栏类则支持,但如果没有必要也可以选择不执行;
- 倒数闭锁类基于AQS类共享模式实现;而循环栅栏类则直接基于可重入锁类,间接基于AQS类独占模式实现,这也是循环栅栏类可以实现循环拦截的核心原因;
- 倒数闭锁类的任意拦截线程异常不会导致其它拦截线程异常;而循环栅栏类的任意拦截线程异常都将导致同批次的拦截线程抛出损坏栅栏异常;
- 倒数闭锁类的线程“拦截”与“计数”是分离的,因此可以拦截与拦截上限不同数量的线程,并且线程释放也可以由拦截线程以外的线程控制;而循环栅栏类的线程“拦截”与“计数”则是绑定的,因此可拦截线程数量与拦截上限必然等同,并且线程释放也只能由拦截线程控制。
使用
创建
- public CountDownLatch(int count) —— 创建指定拦截上限的倒数闭锁。
方法
-
public void countDown() —— 倒数 —— 统计当前倒数闭锁的拦截线程总数,并在拦截线程总数达到拦截上限时释放所有拦截线程。拦截线程总数达到拦截上限后该方法的后续调用将没有任何作用。
-
public void await() throws InterruptedException —— 等待 —— 通过当前倒数闭锁令当前线程无限等待至拦截线程总数达到拦截上限为止。如果当前线程在拦截期间被中断则抛出中断异常。拦截线程总数达到拦截上限后该方法的后续调用将没有任何作用。
-
public boolean await(long timeout, TimeUnit unit) throws InterruptedException —— 等待 —— 通过当前倒数闭锁令当前线程有限等待至拦截线程总数达到拦截上限并返回true为止,超出指定等待时间则返回false。如果当前线程在拦截期间被中断则抛出中断异常。拦截线程总数达到拦截上限后该方法的后续调用将没有任何作用。
-
public long getCount() —— 获取总数 —— 获取当前倒数闭锁剩余可拦截的线程总数。
模板
/*** 线程池执行器(注意!!!此处只是为了快速获取线程池执行器,开发环境不推荐使用* newFixedThreadPool(int nThreads)方法创建线程池执行器,易造成OOM。)*/
private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(5);public static void main(String[] args) {System.out.println("程序执行开始!!!");CountDownLatch countDownLatch = new CountDownLatch(5);for (int i = 0; i < 5; i++) {EXECUTOR_SERVICE.submit(() -> {// 执行业务逻辑。System.out.println("当前线程的线程ID为:" + Thread.currentThread().getId());// 倒数总数。countDownLatch.countDown();});}try {// 无限等待。System.out.println("程序等待中...");countDownLatch.await();System.out.println("程序等待结束!!!");// 有限等待 ~ 此处限制为3秒,3秒后即使总数未归零当前线程也会自动唤醒并通过。// boolean result = countDownLatch.await(3, TimeUnit.SECONDS);// System.out.println("是否因为总数归零而通过:" + result);} catch (InterruptedException ignored) {// 什么也不做。} finally {System.out.println("程序执行结束!!!");}
}
实现
AQS/安全/同步
AQS类是Java设计用于在并发环境下保护资源的API。AQS类全称AbstractQueuedSynchronizer @ 抽象队列同步器类,其“半”实现了以“同步”为核心的线程管理机制,用于对想要/已经访问资源的线程进行等待/保存/唤醒管理以使之达成/解除同步,该线程管理机制被称简称为同步机制。所谓“同步”是由AQS类定义的概念,并且即使在概念中也极为抽象的存在,原因是其并未像“行走/飞翔/游泳”概念一样被明确,而是类似于“行动”概念一样的再抽象体。如果一定要对“同步”概念进行描述,那么将之大致理解为“规则”是比较准确的,因为达成同步的线程将因为“遵守规则”而实现对受保护资源的安全并发访问。“安全”同样是极为抽象的概念,初学者很容易从数据角度切入而将之片面理解为正确,但由于环境/硬件/需求同样也是程序开发/运行的限制/影响因素,因此“安全”概念实际上也可能基于正确/限流/批次等多种维度被明确。
AQS类将“安全/同步”概念交由子类明确/实现。由于各子类对“安全”概念的明确不同,并且不同的“安全”概念明确又需要制定相应的规则,即明确/实现相应的“同步”概念予以保证,因此AQS类仅是定义了“安全/同步”概念,而概念的明确/实现则被交由子类负责,故而上文才会说其“半”实现了同步机制。通过对各子类“同步”概念明确/实现的回调,AQS类可以在达成各类同步的同时确保同步机制线程管理功能的统一性。这种编程方式被称为模板模式,Java中基本所有抽象类形式的API都使用了模板模式。AQS类子类对“安全”概念是只需明确而无需实现的,因为其作用仅是为“同步”概念提供明确/实现依据,即我们必须先知道资源对安全访问的实际要求为何,随后才能为之设计相应的访问规则。
倒数闭锁类基于“拦截”规则在内部实现了AQS类子类。需要事先重点说明的是:虽然倒数闭锁类基于AQS类实现,但倒数闭锁类并不是AQS类的子类。倒数闭锁类通过在内部定义/实现AQS类子类并调用其实例的方式实现自身设计。在倒数闭锁类的内部AQS类子类中,“安全”概念被明确为“批次”,因为倒数闭锁类的核心作用即为确保指定数量的线程同时开始/结束对资源的访问;而“同步”概念则被明确为“拦截”,即不允许线程在达到指定数量前开始/结束对资源的访问。当创建倒数闭锁时,构造方法会联动创建内部AQS类子类实例并保存在[sync @ 同步]中,由于同步的达成便意味着线程对规则的遵守,因此倒数闭锁拦截/释放线程的本质即为通过[同步]等待/达成同步。
倒数闭锁类内部只实现了一种AQS类子类。倒数闭锁类在设计上不存在访问策略,或者说只存在非公平策略,因此其内部也只实现了一种AQS类子类。该AQS类子类会在创建倒数闭锁时被相应的创建并保存在[同步]中…其具体名称及作用如下所示:
Sync @ 同步类 ~ 同步类是AQS类的直接子类,其核心作用在于为AQS类“两荤两素,四菜一汤”中的tryAcquireShared(int acquires)/tryReleaseShared(int releases)方法提供通用实现。
比起同步的达成/解除,倒数闭锁类更注重对同步机制的灵活使用。如果说基于AQS类实现的锁类API更注重于同步达成/解除的话,那以“三剑客”为首的线程控制工具就更注重于对同步机制的灵活使用。这两者的区别在于前者中的同步机制只被单纯作为令同步达成/解除的辅助手段,即如果不是因为线程为了达成/解除同步而可能需要等待,那么同步机制就完全没有存在的必要;而后者中的同步机制则转而变为了同步达成/解除的辅助对象,即如果不是因为同步机制需要同步的达成/解除作为其等待/唤醒线程的判断条件,则同步的达成/解除也完全没有存在的意义。故而我们可以知道的是:包含倒数闭锁类在内的线程控制工具其拦截/释放线程的本质实际上都是通过对同步机制的灵活调用而令线程进入/退出有限/无限等待状态,由于进入/退出等待状态的线程将停止/恢复对任务的执行,因此就变相达到了线程拦截/释放的效果。该知识点会在下文讲解拦截/释放时详述。
状态/获取/释放/模式
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类子类采用了共享模式,并将[状态]的获取明确/实现为0的等值判断,而将[状态]的释放明确/实现为递减。由于倒数闭锁类只基于共享特性实现的原因,倒数闭锁类的内部AQS类子类只对[状态]存在/获取/释放的共享定义进行了实现以提供功能支持。我们首先需要知道的是:由于倒数闭锁类在设计上存在拦截上限的原因,其内部需要对已拦截的线程进行统计以判断总数是否已达到拦截上限,并将之作为是否释放拦截线程的判断依据。倒数闭锁类选择使用减法计数来统计拦截线程总数,目的是为了减少运行时的内存开销。因为如果使用加法计数来统计拦截线程总数,则倒数闭锁类还需要设计额外字段来保存拦截上限,但如果使用减法计数的话则拦截上限就可以在创建倒数闭锁时直接保存在[状态]中。而由此我们也可以知道的是:[状态]的释放被明确/实现为递减的本质实际上是在统计拦截线程的总数,即[状态]的每次递减都意味着拦截线程总数的递增,而[状态]为0则意味着拦截线程的总数已达拦截上限。[状态]的获取被明确/实现为0的等值判断则会被倒数闭锁用于判断是否应该拦截线程,因为[状态]不为0便意味着拦截线程的总数尚未达到拦截上限。倒数闭锁类通过上述判断/计数来调用AQS类的同步机制以实现对线程的拦截/释放,该知识点会在下文讲解拦截/释放时详述。
同步队列
AQS类使用同步队列保存尝试达成同步失败的线程。同步队列是AQS类用于保存线程的数据结构,当线程尝试同步失败时,AQS类会将线程封装为节点并尾插至同步队列中有限/无限等待。一个值得思考的问题是:既然同步机制会控制线程循环尝试达成同步,那又为什么要将尝试同步失败的线程加入到同步队列中等待呢?实际上该问题的答案在上文中其实已经提及过,即[状态]获取尝试的失败通常都带有[状态]不存在的隐性含义。而在[状态]不存在的情况下,令线程持续不断地进行必然/大概率失败的同步尝试不过只是徒增开销的无意义行为,因此令线程在同步队列中等待实际上是避免无意义开销的有效手段。当[状态]因线程彻底释放而存在,或同步因为中断/超时而取消时,等待中的线程将被信号/中断/超时唤醒并再次/取消尝试同步。
同步队列是逻辑队列。所谓逻辑队列是指同步队列并不是类似LinkedList @ 链接列表的对象,其本质只是单纯的链表,而AQS类则持有其[head/tail @ 头/尾节点]的引用。由于同步队列的节点类在结构设计上支持持有[前驱/后继节点]的引用,因此AQS类只要持有了[头/尾节点]就相当于持有了整个同步队列。
同步队列是AQS类为子类提供的公平策略实现方案。同步队列是标准FIFO @ 先入先出队列,线程会从队列的尾部插入,并在同步达成后从头部移除。由于AQS类规定只有位于同步队列头部的线程才具备同步资格,因此在同步队列中同步的达成必然是公平的,即在同步队列中成功达成同步的线程必然是访问时间最早/等待时间最久的。此外虽然AQS类只会在线程尝试达成同步失败时将之插入同步队列中,但是否失败却是由子类全权负责明确/实现的,因此除[状态]不存在而导致的被动失败外,AQS类子类还可以先通过“故意/计划”性质的主动失败令线程在加入同步队列后再进行真正的尝试同步,从而确保线程同步达成的必然公平,因此AQS类子类可通过同步队列实现自身的公平策略。而事实上,所有基于AQS类的API其公平策略(如果存在的话)也确实都是如此实现的…至少我没有发现例外。
同步队列是低效的。我们其实不难理解这一点,因为无论同步队列中保存了多少线程,按照AQS类的设定也就只有头部线程可以尝试达成同步,因此同步队列中同步实际上就是在单线程环境中达成的,故而性能低下也是可以预见的。而也正是因为该原因,除非[状态]确实不存在,否则正如上文所说AQS类其实并不建议子类将尝试同步失败的线程交由同步机制负责重试,而是推荐其在尝试方法中嵌套循环尝试至成功或因为[状态]不存在而失败为止,因为这将导致线程在[状态]存在的情况下被加入同步队列中有限/无限等待。虽说这并不会对[状态]获取的成功必然性造成影响,但却会对AQS类子类的性能造成严重的损害。毕竟只有在尝试同步必然/大概率失败的情况下,将线程加入同步队列中等待才有益于减少连续尝试造成的性能损失。
拦截/释放
倒数闭锁类通过tryAcquireShared(int acquires)方法尝试判断[状态]是否为0。AQS类的共享模式流程通过调用tryAcquireShared(int acquires)方法对[状态]进行尝试性的共享获取,而又因为[状态]共享获取的概念被定义0的等值判断,因此tryAcquireShared(int acquires)方法在倒数闭锁类中的实际作用即为尝试判断[状态]是否为0。判断[状态]是否为0的含义是判断拦截线程的总数是否已达拦截上限,由于倒数闭锁类使用减法计数来统计拦截线程的总数,因此[状态]未归零就意味着拦截线程总数尚未达到拦截上限,同时也意味着倒数闭锁需要继续拦截线程。我们已知倒数闭锁类拦截线程的本质是通过灵活调用同步机制而令线程进入有限/无限等待状态,并且上文也已说过在同步机制中线程只有在获取尝试失败的情况下才能被加入同步队列中等待,故而在[状态]不为0的情况下tryAcquireShared(int acquires)方法会返回false来宣告获取尝试的失败…相关流程如下:
倒数闭锁类通过tryReleaseShared(int releases)方法尝试递减[状态]。AQS类的共享模式流程通过调用tryReleaseShared(int releases)方法对[状态]进行尝试性的共享释放,而又因为[状态]共享释放的概念被定义为[状态]的递减,因此tryReleaseShared(int releases)方法在倒数闭锁类中的实际作用即为尝试递减[状态]。递减[状态]的本意是统计拦截线程的总数,由于这期间可能有其它线程并发递减[状态],因此赋值CAS会循环执行至成功或[状态]已归零为止以保证释放尝试的必然成功,这一点恰好对应了上文“AQS类子类需要确保[状态]释放尝试的必然成功”的内容。当[状态]从拦截上限被成功递减为0时意味着拦截线程的总数已达拦截上限,同时也意味着倒数闭锁需要释放所有拦截线程。我们已知倒数闭锁类释放线程的本质是通过灵活调用同步机制而令线程退出有限/无限等待状态,即信号唤醒处于有限/无限等待状态中的线程,并且上文也已说过同步机制的共享模式会在[状态]彻底释放时一次唤醒同步队列中等待的所有线程,因此当[状态]被递减为0时tryReleaseShared(int releases)方法会返回true来宣告[状态]的彻底释放…相关流程如下:
需要特别注意的是:由于当[状态]归零后后续线程将因为tryAcquireShared(int acquires)方法永远返回true而不再加入同步队列中等待,因此此时的同步队列中将不存在任何等待线程,这也正符合倒数闭锁类只支持一次拦截的设计。但又因为tryReleaseShared(int releases)方法后续也会因为[状态]为0而永远返回true并导致同步机制执行无意义的信号唤醒,因此为了避免无谓的性能损耗tryReleaseShared(int releases)方法只会在[状态]被递减为0时返回true,而后续的判断为0则只会返回false。
有限拦截的线程可能被提前释放。理论上,被拦截的线程只有在拦截线程总数达到拦截上限时才会被统一释放。但这并不是绝对的,因为如果线程拦截是通过await(long timeout, TimeUnit unit)方法实现的,那其就可能因为拦截时间超过指定时间而提前释放,并且这并不会导致其它拦截线程被提前释放。
某拦截线程的异常不会导致其它拦截线程异常。当某线程在被倒数闭锁拦截期间因为中断等原因抛出异常时,其并不会导致其它拦截线程发生异常或提前释放。这一点与循环栅栏类是不同,因为当相同的情况发生时,循环栅栏中被同一批次拦截的其它线程都将抛出损坏栅栏异常。
“拦截”与“计数”的拆分
倒数闭锁类最大的特性在于将线程“拦截”与“计数”进行了拆分。如何理解所谓的拆分呢?其本质是指倒数闭锁类没有将计数功能整合进拦截功能相关的方法中,即await()/await(long timeout, TimeUnit unit)方法并不具备统计拦截线程总数的能力,而是由countDown()方法专属负责,这与循环栅栏类直接通过await()/await(long timeout, TimeUnit unit)方法同时对线程进行“拦截”与“计数”的设计是不同的。拆分使得倒数闭锁类无法像循环栅栏类一样实现循环拦截,因为“总数还原”与“线程唤醒”无法在一次原子操作中完成,而这就可能导致唤醒后一批被拦截的线程。而虽说其基于允许并发的AQS类共享模式实现也是一大原因,但相比而言拆分更加核心,因为在拆分设计下即使和循环栅栏类一样使用不允许并发的AQS类独占模式也是无法做到循环拦截的,因此倒数闭锁类基于AQS类共享模式实现更多是为了提升性能。而拆分带来的好处是倒数闭锁类可以拦截与拦截上限不同数量的线程,并且线程释放也可以由拦截线程以外的外部线程控制,这显著增强了拦截的灵活性与可控性。例如下文代码就基于拆分设计实现了控制不同线程内任务执行顺序的功能,而这是使用循环栅栏类所做不到的。
/*** 线程池执行器(注意!!!此处只是为了快速获取线程池执行器,开发环境不推荐使用* Executors.newFixedThreadPool(int nThreads)方法创建线程池执行器,易造成OOM。)*/
private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(2);public static void main(String[] args) {// 实例化倒数闭锁A/B。CountDownLatch countDownLatchA = new CountDownLatch(1);CountDownLatch countDownLatchB = new CountDownLatch(1);// 使用线程池执行器执行任务A。EXECUTOR_SERVICE.submit(() -> {try {// 拦截任务A,直至主线程递减的倒数闭锁A的总数至0时通过。System.out.println("任务A开始执行...");System.out.println("任务A被拦截...");countDownLatchA.await();System.out.println("任务A被释放...");System.out.println("任务A恢复执行...");// 等待2秒,模拟任务A的执行耗时。Thread.sleep(2000);} catch (InterruptedException ignored) {// 什么也不做。}System.out.println("任务A执行结束,释放任务B。");countDownLatchB.countDown();});// 使用线程池执行器执行任务B。EXECUTOR_SERVICE.submit(() -> {try {System.out.println("任务B开始执行...");System.out.println("任务B执行中...");// 等待2秒,模拟任务B在被拦截前的执行耗时。该过程可与任务A并发,否则完全不允许并发的// 任务应该尽可能放在同一个线程中执行。Thread.sleep(2000);System.out.println("任务B被拦截...");countDownLatchB.await();System.out.println("任务B被释放...");System.out.println("任务B恢复执行...");// 等待2秒,模拟任务B在被释放后的执行耗时。Thread.sleep(2000);} catch (InterruptedException ignored) {// 什么也不做。}System.out.println("任务B执行结束。");});// 等待1秒,避免日志输出混乱。try {Thread.sleep(1000);} catch (InterruptedException ignored) {// 什么也不做。}System.out.println("主任务释放任务A。");countDownLatchA.countDown();
}
相关文章:
Java Lock CountDownLatch 总结
前言 相关系列 《Java & Lock & 目录》(持续更新)《Java & Lock & CountDownLatch & 源码》(学习过程/多有漏误/仅作参考/不再更新)《Java & Lock & CountDownLatch & 总结》(学习总…...
vue+spreadjs开发
创建vue3项目 pnpm create vite --registryhttp://registry.npm.taobao.org安装spreadjs包 pnpm install "grapecity-software/spread-sheets17.1.7" "grapecity-software/spread-sheets-resources-zh17.1.7" "grapecity-software/spread-sheets-vu…...
针对初学者的PyTorch项目推荐
文章目录 1. MNIST手写数字识别2. CIFAR-10图像分类3. 图像风格迁移4. 文本生成(使用RNN)5. 简单的问答系统6. 简单的生成对抗网络(GAN)7. 简单的推荐系统 对于初学者来说,选择一些简单且具有教育意义的项目来实践PyTo…...
Helm Chart文件介绍
介绍(这个还没有完善 ,目前在找工作呢) Helm是Kubernetes的包管理器,类似于Ubuntu中的apt、CentOS中的yum或Python中的pip,可以快速查找、下载和安装软件包。Helm主要由客户端组件helm和服务端组件Tiller组成…...
1Panel 是新一代的 Linux 服务器运维管理面板
1Panel 是一款新一代的 Linux 服务器运维管理面板,旨在通过现代化的 Web 界面帮助用户轻松管理 Linux 服务器。它集成了主机监控、文件管理、数据库管理、容器管理等功能,并且支持多语言和国际化,包括英语、中文(繁体)和日语。以下是 1Panel …...
Qml-ShaderEffect的使用
Qml-ShaderEffect的使用 ShaderEffect的概述 ShaderEffect使用自定义的顶点和片段着色器用于渲染一个矩形。用于在qml场景中添加阴影、模糊、着色和页面卷曲等效果。 Qt5和Qt6中ShaderEffect有一定区别,在Qt6中由于支持不同的渲染API,ShaderEffect是用…...
鸿蒙next之axios二次封装并携带cookie
由于官方提供的ohos.net.http模块,直接使用不是很灵活,就引入了第三方ohos/axios库。 以下是引入axios并进行二次封装的步骤: 1、DevEco Studio打开终端输入命令安装插件 ohpm install ohos/axios 2、新建RequestUtil.ets import { JSON, …...
WordPress中最值得推荐的AI插件:专家级指南
WordPress平台上,人工智能(AI)技术不断发展,为用户提供了丰富的工具和功能。对于有经验的用户,这些工具不仅能提升网站性能和用户体验,还能在安全和互动方面提供更多支持。在这篇文章中,我将为大…...
HTTP介绍及请求过程
HTTP(HyperText Transfer Protocol),即超文本传输协议,是一种用于分布式、协作式和超媒体信息系统的应用层协议。以下是关于 HTTP 的详细介绍: 一、基本概念 定义与作用: HTTP 是互联网上应用最为广泛的一种网络协议,它定义了客户端和服务器之间请求和响应的标准方式。…...
WebGL进阶(五)-可视域
理论基础: 顶点着色器 Vertex Shader 主要是负责处理顶点位置、顶点颜色、顶点向量等顶点的数据;处理一些顶点的变换:例如在进行视图变换和投影变换时MVP矩阵会改变顶点的位置信息。 输入: 顶点着色器输入部分主要是声明&…...
2024性价比家居好物有哪些?推荐五款值得每个家庭拥有的好物品牌!
每年双11的时候我都特别喜欢买一些家居好物,今年双11也不例外,经过我一两周的精心挑选,专门选了五款性价比高的家居好物,接下来给大家分享一下! 家居好物一、希亦ACE Pro内衣洗衣机 我买过、评测过的内衣洗衣机&#…...
字节青训-查找热点数据问题
问题描述 给你一个整数数组 nums 和一个整数 k,请你返回其中出现频率前 k 高的元素。请按升序排列。 1 < nums.length < 10^5k 的取值范围是 [1, 数组中不相同的元素的个数]题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合…...
Codeforces Round 981 (Div. 3) (A~F)
文章目录 A. Sakurako and Kosuke思路code B. Sakurako and Water思路code C. Sakurakos Field Trip思路code D. Kousukes Assignment思路code E. Sakurako, Kosuke, and the Permutation思路code F. Kosukes Sloth思路code Codeforces Round 981 (Div. 3) A. Sakurako and Ko…...
shell脚本实例(4)while实现1+...+100,linux新增用户
while实现1到100求和 #!/bin/bash/ s0 i1 #-le小于等于 while [ $i -le 100 ] dos$[ $s$i ]i$[ $i1 ] done echo $s echo $i 执行结果如下 修改用户名密码脚本 #!/bin/bash/ #提示用户输入用户名 read -p "请输入用户名:"username useradd $username #提…...
docker XML详解
下列为一个基本的运行docker镜像文件 {"Id": "62a82b0e69930e54c291095f632adde58dd0b247adba3a048385a55c87e38eba","Created": "2024-07-11T04:00:09.36091853Z","Path": "java","Args": ["-ja…...
web前端边框详解,弹性盒子的使用(仿写购物网页)
边框详解 1. 边框宽度(border - width) - 具体取值:可以是具体的长度值,如 px (像素)、 pt (点)、 em (相对单位)等。例如, border - width: 2px…...
【ACM出版,EI稳定检索,九大高校联合举办, IEEE Fellow支持】2024年计算机视觉与艺术研讨会(CVA 2024)
在线投稿:学术会议-学术交流征稿-学术会议在线-艾思科蓝 2024年计算机视觉与艺术国际学术会议(CVA 2024)作为2024年人工智能、数字媒体技术与交互设计国际学术会议(ICADI 2024)的分会。此次大会旨在汇聚全球在计算机视觉与艺术…...
认识软件测试
博主主页: 码农派大星. 数据结构专栏:Java数据结构 数据库专栏:MySQL数据库 JavaEE专栏:JavaEE 软件测试专栏:软件测试 关注博主带你了解更多知识 1. 什么是测试? 测试在⽣活中处处可⻅ 例子: 对某款购物软件进⾏测试 启动测试:点击软件图标&#…...
poi处理excel文档时,与lombok的@Accessors(chain = true)注解冲突
poi在反射封装数据时会判断set方法的返回是不是Void,加上Accessors会造成NoSuchMethodException异常...
我接触csdn中的c++的时间
大家好,我是AC使者,不知不觉我也来到CSDN半年了!在这半年我也看到了自身的不足,我也还有了很多粉丝,所以我今天来总结一下这半年的东西。 第一篇--------结构体数组 关于结构体数组的理解-CSDN博客 第二篇--------字…...
go语言多态性(接口interface)的使用
前言 在Go语言中,接口类型(interface)完全可以作为一个函数的参数。这是Go语言多态性的一个重要体现,允许函数接受任何实现了接口中定义的方法的类型的实例。 一、接口(interface)定义 type Reader inte…...
如何将markdown文件转换为pdf
最近笔者在用vscode写markdown,但是提交时往往需要交pdf。所以就涉及到如何将markdown转化为pdf格式。 首先,需要在vscode上安装插件 markdown Preview Enhanced 之后在vscode的右上角即可看到下述图标,点击,vscode右半面就会显示…...
【python实操】python小程序之测试报告
引言 python小程序之测试报告 文章目录 引言一、测试报告1.1 概念1.1.1 使用Pytest和Allure生成测试报告1.1.2 使用unittest和HTMLTestRunner生成测试报告1.1.3 总结 1.2 题目1.3 代码1.3 代码解释 二、思考 一、测试报告 1.1 概念 python生成测试报告,常用的方法包…...
【Java基础】2、Java基础语法
f2/fnf2:选中点中的文件名 1.注释 为什么要有注释? 给别人和以后的自己可以看懂的解释 注释含义 注释是在程序指定位置的说明性信息;简单理解,就是对代码的一种解释 注释分类 单行注释 //注释信息 多行注释…...
MATLAB基础应用精讲-【数模应用】本量利分析(Cost-Volume-Profit Analysis)
目录 前言 几个高频面试题目 本量利分析与量本利分析的区别 算法原理 发展历程 几个相关概念 什么是CVP分析 基本假设 注意事项 本量利分析的作用 基本原理 多种产品量本利分析 盈亏平衡分析 目标利润分析 敏感性分析 边际分析 本量利分析基本模型 应用场景 …...
实习冲刺Day7
算法题 合并两个有序链表 class Solution { public:void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {for (int i 0; i<n; i) {nums1[m i] nums2[i];//直接将num2的数据插入到num1的尾部}sort(nums1.begin(), nums1.end());//排…...
《Python游戏编程入门》注-第4章1
《Python游戏编程入门》的第4章是“用户输入:Bomb Cathcer游戏”,通过轮询键盘和鼠标设备状态实现Bomb Cathcer游戏。 1 Bomb Cathcer游戏介绍 “4.1 认识Bomb Cathcer游戏”内容介绍了Bomb Cathcer游戏的玩法,即通过鼠标来控制红色“挡板”…...
一些硬件知识【2024/10/29】
千兆以太网有8条信号线,百兆以太网有4条线: 网络变压器构造图: 百兆以太网拓扑: BOB Smith电路: 【以太网接口电 路设计】https://www.bilibili.com/video/BV1i3411u7bv?vd_source3cc3c07b09206097d0d8b0aefdf07958&a…...
利用弱监督学习在全切片病理图像中检测和分型基底细胞癌|文献速递-基于生成模型的数据增强与疾病监测应用
Title 题目 Detection and subtyping of basal cell carcinoma in whole-slide histopathology using weakly-supervised learning 利用弱监督学习在全切片病理图像中检测和分型基底细胞癌 01 文献速递介绍 基底细胞癌 (BCC) 的发病率正在给病理诊断带来压力。BCC 的发病率…...
leetcode刷题笔记——15.三数之和
一、问题描述 给定一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]],使得: i ! j、i ! k 且 j ! k nums[i] nums[j] nums[k] 0 需要返回所有和为 0 的三元组,且这些三元组不能重复。 输入输出 输入: 整…...
照片管理网站模板下载/网站设计需要什么
开发工具是Android Studio,实现了一个中英互译的安卓app,调用科大讯飞的语音识别、语音合成api以及百度翻译api,需要科大讯飞的appid,以及百度翻译的appid和密钥。 App运行截图: 科大讯飞的语音识别、语音合成api调用流程(SDK调用方式)&#…...
广西高端网站建设公司/百度收录提交工具
1、使用printf应当说是类型不安全的。所以才引入了C的流输入输出。 比如: #include "stdint.h" #include "iostream" using namespace std; int main() { int64_t a 1; int b 2; uint32_t uin 1; printf("%p %p\n", &a, &…...
开网站做备案需要什么资料/企业培训权威机构
文章目录Rust的智能指针--RefCell<T>注意:Rust的智能指针–RefCell 现在我定义一个接口Messager,用来send消息。 pub trait Messager {fn send(&self, msg: &str); }然后一个LimitTracker类型根据指定的配额来send消息* pub struct Limi…...
wordpress为什么放弃/怎么让关键词快速排名首页
关于C,hanoi塔的递归问题一直是个经典问题,我们学习数据结构的时候也会时常用到, 因为它的时间复杂度和空间复杂度都很高,我们在实际的应用中不推荐使用这种算法,移动n个盘子, 需要2的n次幂减一步ÿ…...
wordpress页面加载时间代码/网站关键词优化wang
最近我不得不在 Laravel 7 实现 通用唯一识别码 ( UUIDs ),并遇到一些问题。我希望这帖子可为其他正在做相同事情的人解惑。 使用 UUIDs 的高级理由 A) 它们从你的 统一资源定位符 移除编号的 身份识别号 ,故用户不能看到你的应…...
昆明网站建设方案报价/北京百度推广排名优化
美团 一面: 1、ConcurrentHashMap实现原理 2、HashMap实现原理 3、锁的实现原理 4、synchronized和重入所实现原理以及区别 5、一个char[]数组,里面有空格,以&结束。 6、jvm内存模型,都存什么。以及垃圾回收算法,…...