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() ------ 获取总数 ------ 获取当前倒数闭锁剩余可拦截的线程总数。++

模板

java 复制代码
/**
 * 线程池执行器(注意!!!此处只是为了快速获取线程池执行器,开发环境不推荐使用
 * 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类共享模式实现更多是为了提升性能。而拆分带来的好处是倒数闭锁类可以拦截与拦截上限不同数量的线程,并且线程释放也可以由拦截线程以外的外部线程控制,这显著增强了拦截的灵活性与可控性。例如下文代码就基于拆分设计实现了控制不同线程内任务执行顺序的功能,而这是使用循环栅栏类所做不到的。

java 复制代码
/**
 * 线程池执行器(注意!!!此处只是为了快速获取线程池执行器,开发环境不推荐使用
 * 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();
}
相关推荐
xlsw_3 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹4 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫4 小时前
泛型(2)
java
超爱吃士力架5 小时前
邀请逻辑
java·linux·后端
南宫生5 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石5 小时前
12/21java基础
java
李小白665 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp5 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
装不满的克莱因瓶6 小时前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb