CountDownLatch 并发编程中的同步利器

文章目录

  • CountDownLatch 并发编程中的同步利器
    • 一、`CountDownLatch` 基础概念
      • 1.1 什么是 `CountDownLatch`?
      • 1.2 `CountDownLatch` 的核心方法
      • 1.3 基本使用示例
    • 二、`CountDownLatch` 实战应用
      • 2.1 应用场景一:并行任务协调
      • 2.2 应用场景二:多阶段并发处理
      • 2.3 应用场景三:并发测试
    • 三、源码解析
      • 3.1 `CountDownLatch` 的构造与字段
      • 3.2 核心方法实现
        • 3.2.1 await 方法
        • 3.2.2 countDown 方法
      • 3.3 工作流程图解
    • 四、`CountDownLatch` 的高级用法
      • 4.1 带超时的等待
      • 4.2 结合 CyclicBarrier 实现复杂同步
    • 4.3 注意事项与最佳实践
    • 五、`CountDownLatch` 与其他同步工具的对比
      • 5.1 `CountDownLatch` vs CyclicBarrier
      • 5.2 `CountDownLatch` vs Semaphore
      • 5.3 `CountDownLatch` vs Join
    • 六、面试中的 `CountDownLatch` 问题解析
      • 6.1 基础概念题
      • 6.2 代码实现题
      • 6.3 原理分析题

在这里插入图片描述

​ 在多线程并发编程中,线程同步是一个永恒的话题。当我们需要等待多个线程完成某些操作后再继续执行,或者需要等待某个条件满足后才能开始执行,CountDownLatch 便成为了一把利器。它就像一个倒计时器,只有当计数归零时,等待的线程才能继续执行。

一、CountDownLatch 基础概念

1.1 什么是 CountDownLatch

CountDownLatch 是 Java 并发包(java.util.concurrent)中的一个同步工具类,它允许一个或多个线程等待一系列指定操作的完成。CountDownLatch 的核心思想是维护一个计数器,每完成一个操作,计数器减一,当计数器归零时,等待的线程被释放继续执行。

这听起来有点抽象,我们可以通过一个简单的比喻来理解:想象你是一个项目经理,手下有5个开发人员,每人负责一个模块。只有当这5个模块都开发完成后,整个项目才能进入测试阶段。CountDownLatch 就相当于一个跟踪这5个模块开发进度的工具,每完成一个模块,计数器减一,当计数器变为0时,测试团队就可以开始工作了。

1.2 CountDownLatch 的核心方法

CountDownLatch 主要有两个核心方法:

  • countDown():递减计数器,表示一个操作已完成
  • await():等待计数器归零,如果计数器大于0,则当前线程进入等待状态,此外还有一些其他实用方法:
    • await(long timeout, TimeUnit unit):等待计数器归零,但最多等待指定的时间
    • getCount():获取当前计数器的值

1.3 基本使用示例

下面是一个简单的示例,演示 CountDownLatch 的基本使用:

import java.util.concurrent.CountDownLatch;public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException {// 创建一个计数器为5的CountDownLatchCountDownLatch latch = new CountDownLatch(5);System.out.println("主线程开始等待所有工作线程完成...");// 创建并启动5个工作线程for (int i = 0; i < 5; i++) {final int workerId = i + 1;new Thread(() -> {try {// 模拟工作线程执行任务System.out.println("工作线程" + workerId + "开始执行任务");Thread.sleep((long) (Math.random() * 1000));System.out.println("工作线程" + workerId + "完成任务");// 任务完成,计数器减一latch.countDown();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}).start();}// 主线程等待所有工作线程完成latch.await();System.out.println("所有工作线程已完成,主线程继续执行");}
}

在这个例子中,主线程创建了5个工作线程,然后调用latch.await()等待所有工作线程完成。每个工作线程完成任务后调用latch.countDown()将计数器减一。当所有5个工作线程都完成任务后,计数器归零,主线程从 await() 方法返回并继续执行。

二、CountDownLatch 实战应用

2.1 应用场景一:并行任务协调

CountDownLatch 最常见的应用场景是协调并行任务的执行。当一个复杂任务可以分解为多个独立的子任务并行执行时,我们可以使用 CountDownLatch 来等待所有子任务完成后再进行下一步操作。

下面是一个模拟并行加载系统模块的例子:

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class SystemInitializer {public static void main(String[] args) throws InterruptedException {// 需要初始化的模块列表List<String> modules = Arrays.asList("数据库连接模块", "缓存服务模块", "消息队列模块", "用户认证模块", "日志服务模块");int moduleCount = modules.size();// 创建与模块数量相同的计数器CountDownLatch latch = new CountDownLatch(moduleCount);System.out.println("系统启动中,等待所有模块初始化...");// 创建固定大小的线程池ExecutorService executor = Executors.newFixedThreadPool(Math.min(moduleCount, 3));// 启动时间long startTime = System.currentTimeMillis();// 为每个模块创建初始化任务for (String module : modules) {executor.submit(() -> {try {initModule(module);} finally {// 模块初始化完成,计数器减一latch.countDown();System.out.println("模块 [" + module + "] 初始化完成,还剩 " + latch.getCount() + " 个模块");}});}// 等待所有模块初始化完成latch.await();// 计算总耗时long endTime = System.currentTimeMillis();System.out.println("所有模块初始化完成,系统启动成功!总耗时: " + (endTime - startTime) + "ms");// 关闭线程池executor.shutdown();}private static void initModule(String moduleName) {System.out.println("开始初始化模块: " + moduleName);try {// 模拟模块初始化耗时long sleepTime = (long) (Math.random() * 2000 + 1000);Thread.sleep(sleepTime);System.out.println("模块 [" + moduleName + "] 初始化耗时: " + sleepTime + "ms");} catch (InterruptedException e) {Thread.currentThread().interrupt();System.out.println("模块 [" + moduleName + "] 初始化被中断");}}
}

在这个例子中,我们使用 CountDownLatch 来协调系统各个模块的初始化过程。系统启动时需要初始化多个模块,每个模块的初始化可以并行进行,但只有当所有模块都初始化完成后,系统才能正常运行。

2.2 应用场景二:多阶段并发处理

有时我们需要将一个大型任务分为多个阶段,每个阶段都需要多个线程协同完成,且下一阶段必须等待上一阶段完全完成后才能开始。这种情况下,我们可以为每个阶段创建一个 CountDownLatch

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class MultiPhaseTask {public static void main(String[] args) throws InterruptedException {int threadCount = 3;// 创建线程池ExecutorService executor = Executors.newFixedThreadPool(threadCount);// 第一阶段的计数器CountDownLatch phase1Latch = new CountDownLatch(threadCount);// 第二阶段的计数器CountDownLatch phase2Latch = new CountDownLatch(threadCount);// 第三阶段的计数器CountDownLatch phase3Latch = new CountDownLatch(threadCount);System.out.println("开始执行多阶段任务...");// 启动所有工作线程for (int i = 0; i < threadCount; i++) {final int workerId = i + 1;executor.submit(() -> {try {// 第一阶段:数据准备System.out.println("工作线程" + workerId + "开始第一阶段:数据准备");Thread.sleep((long) (Math.random() * 1000));System.out.println("工作线程" + workerId + "完成第一阶段");phase1Latch.countDown();// 等待所有线程完成第一阶段phase1Latch.await();System.out.println("所有线程完成第一阶段,工作线程" + workerId + "开始第二阶段");// 第二阶段:数据处理Thread.sleep((long) (Math.random() * 1500));System.out.println("工作线程" + workerId + "完成第二阶段");phase2Latch.countDown();// 等待所有线程完成第二阶段phase2Latch.await();System.out.println("所有线程完成第二阶段,工作线程" + workerId + "开始第三阶段");// 第三阶段:结果汇总Thread.sleep((long) (Math.random() * 800));System.out.println("工作线程" + workerId + "完成第三阶段");phase3Latch.countDown();} catch (InterruptedException e) {Thread.currentThread().interrupt();}});}// 等待所有阶段完成phase3Latch.await();System.out.println("所有阶段都已完成,任务结束");// 关闭线程池executor.shutdown();}
}

在这个例子中,我们将任务分为三个阶段:数据准备、数据处理和结果汇总。每个阶段都需要多个线程共同完成,且只有当所有线程都完成当前阶段后,才能进入下一阶段。

2.3 应用场景三:并发测试

CountDownLatch 在性能测试中也有重要应用,特别是需要模拟大量并发请求的场景。下面是一个模拟并发请求的例子:

import java.util.concurrent.CountDownLatch;startLatch.await();// 记录请求开始时间long startTime = System.currentTimeMillis();System.out.println("请求" + requestId + "开始执行");// 模拟发送HTTP请求boolean success = sendHttpRequest(requestId);// 记录请求结束时间并计算耗时long endTime = System.currentTimeMillis();long duration = endTime - startTime;// 根据请求结果更新计数器if (success) {successCount.incrementAndGet();System.out.println("请求" + requestId + "成功,耗时: " + duration + "ms");} else {failureCount.incrementAndGet();System.out.println("请求" + requestId + "失败,耗时: " + duration + "ms");}} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {// 请求完成,计数器减一endLatch.countDown();}});}// 记录测试开始时间long testStartTime = System.currentTimeMillis();// 发出开始信号,所有线程同时开始执行System.out.println("所有请求准备就绪,开始并发测试...");startLatch.countDown();// 等待所有请求完成boolean allCompleted = endLatch.await(30, TimeUnit.SECONDS);// 记录测试结束时间long testEndTime = System.currentTimeMillis();long totalDuration = testEndTime - testStartTime;// 输出测试结果System.out.println("\n并发测试完成!");if (!allCompleted) {System.out.println("警告:有些请求在超时时间内未完成");}System.out.println("总耗时: " + totalDuration + "ms");System.out.println("成功请求数: " + successCount.get());System.out.println("失败请求数: " + failureCount.get());System.out.println("平均响应时间: " + (totalDuration / concurrentRequests) + "ms");// 关闭线程池executor.shutdown();}// 模拟发送HTTP请求private static boolean sendHttpRequest(int requestId) {try {// 模拟请求处理时间,随机100-500msThread.sleep((long) (Math.random() * 400 + 100));// 模拟偶尔的请求失败,约5%的失败率return Math.random() > 0.05;} catch (InterruptedException e) {Thread.currentThread().interrupt();return false;}}
}

在这个例子中,我们使用两个 CountDownLatch

  • startLatch 用于控制所有线程同时开始,模拟真实的并发请求场景。
  • endLatch 用于等待所有请求完成,以便统计整体测试结果。

这种方式可以准确测量系统在高并发情况下的性能表现,是性能测试的常用手段。

三、源码解析

了解 CountDownLatch 的内部实现原理,有助于我们更好地使用它。CountDownLatch 的实现基于 AQS(AbstractQueuedSynchronizer)框架,这也是 Java 并发包中许多同步器的基础。

3.1 CountDownLatch 的构造与字段

先看 CountDownLatch 的构造函数:

public CountDownLatch(int count) {if (count < 0) throw new IllegalArgumentException("count < 0");this.sync = new Sync(count);
}

CountDownLatch 内部维护了一个 Sync 实例,它是 AQS 的子类:

private static final class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = 4982264981922014374L;Sync(int count) {setState(count);}int getCount() {return getState();}protected int tryAcquireShared(int acquires) {return (getState() == 0) ? 1 : -1;}protected boolean tryReleaseShared(int releases) {// Decrement count; signal when transition to zerofor (;;) {int c = getState();if (c == 0)return false;int nextc = c-1;if (compareAndSetState(c, nextc))return nextc == 0;}}
}

Sync 类直接使用 AQS 的 state 变量作为计数器。构造函数中,将 state 设置为指定的计数值。

3.2 核心方法实现

3.2.1 await 方法
public void await() throws InterruptedException {sync.acquireSharedInterruptibly(1);
}

await() 方法调用 AQS 的 acquireSharedInterruptibly(),这个方法会调用 Sync 中重写的 tryAcquireShared() 方法。只有当计数器(state)为 0 时,tryAcquireShared() 才会返回正值,表示可以获取共享锁,否则线程将被阻塞。

3.2.2 countDown 方法
public void countDown() {sync.releaseShared(1);
}

countDown() 方法调用 AQS 的 releaseShared(),这个方法会调用 Sync 中重写的 tryReleaseShared() 方法。tryReleaseShared() 通过 CAS 操作减少计数器的值,当计数器变为 0 时,会返回 true,此时 AQS 会释放所有等待的线程。

3.3 工作流程图解

CountDownLatch 的工作流程可以简化为以下几个步骤:

  1. 创建 CountDownLatch 对象,初始化计数器

  2. 等待线程调用 await() 方法,如果计数器大于 0,线程将被阻塞

  3. 工作线程完成任务后调用 countDown() 方法,计数器减一

  4. 当计数器减至 0 时,AQS 会释放所有等待的线程,它们从 await() 方法返回继续执行

四、CountDownLatch 的高级用法

4.1 带超时的等待

有时我们不希望无限期地等待所有操作完成,这时可以使用带超时参数的 await() 方法:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;public class CountDownLatchWithTimeout {public static void main(String[] args) {// 创建计数器为3的CountDownLatchCountDownLatch latch = new CountDownLatch(3);System.out.println("主线程开始等待工作线程完成...");// 启动工作线程for (int i = 0; i < 3; i++) {final int workerId = i + 1;new Thread(() -> {try {System.out.println("工作线程" + workerId + "开始执行");// 模拟工作线程耗时,第三个线程会故意执行很长时间long sleepTime = (workerId == 3) ? 5000 : 1000;Thread.sleep(sleepTime);System.out.println("工作线程" + workerId + "完成执行");latch.countDown();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}).start();}try {// 等待,但最多等待2秒boolean completed = latch.await(2, TimeUnit.SECONDS);if (completed) {System.out.println("所有工作线程在超时前完成了任务");} else {System.out.println("等待超时,但仍有" + latch.getCount() + "个任务未完成");System.out.println("主线程将继续执行,不再等待未完成的任务");}} catch (InterruptedException e) {Thread.currentThread().interrupt();System.out.println("主线程等待被中断");}System.out.println("主线程继续执行其他操作");}
}

在这个例子中,我们给 await() 方法设置了2秒的超时时间。由于第三个工作线程需要5秒才能完成,因此主线程会在等待2秒后继续执行,即使并非所有工作线程都已完成。

4.2 结合 CyclicBarrier 实现复杂同步

在更复杂的场景中,我们可能需要结合使用 CountDownLatch 和 CyclicBarrier 来实现多阶段、多方向的同步。例如,在一个分布式计算任务中,既需要等待所有工作节点准备就绪后才开始计算,又需要等待所有计算结果返回后才能进行汇总。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ComplexSynchronization {public static void main(String[] args) throws InterruptedException {int workerCount = 5;// 用于等待所有工作线程完成初始化CountDownLatch initLatch = new CountDownLatch(workerCount);// 用于等待所有工作线程完成计算CountDownLatch computeLatch = new CountDownLatch(workerCount);// 用于同步所有工作线程开始计算的时刻CyclicBarrier computeBarrier = new CyclicBarrier(workerCount, () -> {System.out.println("所有工作线程已就绪,开始并行计算");});ExecutorService executor = Executors.newFixedThreadPool(workerCount);System.out.println("主线程开始初始化工作线程...");for (int i = 0; i < workerCount; i++) {final int workerId = i + 1;executor.submit(() -> {try {// 初始化阶段System.out.println("工作线程" + workerId + "开始初始化");Thread.sleep((long) (Math.random() * 1000));System.out.println("工作线程" + workerId + "初始化完成");// 初始化完成,计数器减一initLatch.countDown();// 等待所有线程初始化完成,并同步开始计算computeBarrier.await();// 计算阶段System.out.println("工作线程" + workerId + "开始计算");Thread.sleep((long) (Math.random() * 2000));System.out.println("工作线程" + workerId + "计算完成");// 计算完成,计数器减一computeLatch.countDown();} catch (Exception e) {e.printStackTrace();}});}// 等待所有工作线程初始化完成System.out.println("主线程等待所有工作线程初始化完成...");initLatch.await();System.out.println("所有工作线程初始化完成,等待计算结果...");// 等待所有工作线程计算完成computeLatch.await();System.out.println("所有工作线程计算完成,开始汇总结果");// 模拟结果汇总Thread.sleep(500);System.out.println("结果汇总完成,任务结束");executor.shutdown();}
}

在这个例子中,我们使用了:

  • initLatch(CountDownLatch)等待所有工作线程完成初始化。
  • computeBarrier(CyclicBarrier)同步所有工作线程开始计算的时刻。
  • computeLatch(CountDownLatch)等待所有工作线程完成计算。

这种组合使用可以实现更复杂的多阶段同步场景。

4.3 注意事项与最佳实践

在使用 CountDownLatch 时,有一些注意事项和最佳实践:

  1. 计数器不可重置CountDownLatch 的计数器一旦归零就不能重新设置,如果需要重复使用,应考虑 CyclicBarrier

  2. 防止计数器错误:确保 countDown() 方法被正确调用,特别是在有异常的情况下,通常应该在finally块中调用。

  3. 避免死锁:如果某些线程未能调用 countDown(),等待的线程将永远阻塞,应考虑使用带超时的await()方法。

  4. 资源释放:在所有线程都完成后,及时释放资源,如关闭线程池。

  5. 与线程池结合:通常应将 CountDownLatch 与线程池结合使用,而不是直接创建大量线程。

// 错误的使用方式(没有在finally中调用countDown)
public void wrongWay() {CountDownLatch latch = new CountDownLatch(1);executor.submit(() -> {try {// 如果这里抛出异常,countDown不会被调用doSomething();latch.countDown();} catch (Exception e) {e.printStackTrace();}});
}// 正确的使用方式
public void rightWay() {CountDownLatch latch = new CountDownLatch(1);executor.submit(() -> {try {doSomething();} catch (Exception e) {e.printStackTrace();} finally {// 确保即使出现异常,countDown也会被调用latch.countDown();}});
}

五、CountDownLatch 与其他同步工具的对比

为了更全面地理解 CountDownLatch,我们将它与其他常见的同步工具进行对比:

5.1 CountDownLatch vs CyclicBarrier

  • 重用性:CountDownLatch 的计数器减到0后就不能再用,而 CyclicBarrier 可以通过 reset() 方法重置,可以重复使用。
  • 触发机制:CountDownLatch 是一个线程等待多个线程,或多个线程等待一个线程;CyclicBarrier 是多个线程互相等待,直到所有线程都到达屏障点。
  • 计数方式:CountDownLatch 的计数器只能减少,CyclicBarrier 的计数器可以减少也可以重置。

5.2 CountDownLatch vs Semaphore

  • 作用范围:CountDownLatch 主要用于等待事件;Semaphore 用于控制对资源的并发访问数量。
  • 计数方向:CountDownLatch 计数器只能递减直至归零;Semaphore 的计数器可以递增递减,只要不超过设定的最大值。
  • 使用场景:CountDownLatch 适用于一次性等待场景;Semaphore 适用于限制并发访问资源的场景。

5.3 CountDownLatch vs Join

  • 灵活性:CountDownLatch 可以在任何时候调用 countDown(),而不必等待线程结束;Thread.join() 必须等待线程执行完成。
  • 粒度:CountDownLatch 可以实现更细粒度的控制,一个线程可以多次 countDown();join() 只能等待整个线程执行完毕。
  • 中断处理:两者都支持中断,但 CountDownLatch 可以设置等待超时。

下面是一个简单的对比示例:

import java.util.concurrent.*;}private static void semaphoreExample() throws InterruptedException {// 创建只允许3个线程同时访问的信号量Semaphore semaphore = new Semaphore(3);ExecutorService executor = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {final int id = i;executor.submit(() -> {try {System.out.println("线程" + id + "等待获取许可");semaphore.acquire();System.out.println("线程" + id + "获得许可,开始执行");Thread.sleep(1000);System.out.println("线程" + id + "执行完毕,释放许可");semaphore.release();} catch (InterruptedException e) {Thread.currentThread().interrupt();}});}// 给足够时间让任务执行Thread.sleep(5000);executor.shutdown();}private static void joinExample() throws InterruptedException {Thread[] threads = new Thread[3];for (int i = 0; i < threads.length; i++) {final int id = i;threads[i] = new Thread(() -> {try {System.out.println("线程" + id + "开始执行");Thread.sleep(1000 + id * 500);System.out.println("线程" + id + "执行完毕");} catch (InterruptedException e) {Thread.currentThread().interrupt();}});threads[i].start();}System.out.println("等待所有线程完成...");for (Thread thread : threads) {thread.join();}System.out.println("所有线程已完成!");}
}

这个例子展示了CountDownLatchCyclicBarrierSemaphoreThread.join() 的不同使用方式和特点,有助于理解它们各自的应用场景。

六、面试中的 CountDownLatch 问题解析

CountDownLatch 是Java并发编程面试中的高频话题。下面列出一些常见面试问题及其答案:

6.1 基础概念题

问题1:什么是CountDownLatch?它的主要用途是什么?

答:CountDownLatch是Java并发包中的同步工具类,允许一个或多个线程等待一系列指定操作的完成。它主要用于以下场景:

  • 让主线程等待子线程完成再继续执行
  • 实现多个线程之间的同步
  • 控制并发测试中的并发量
  • 多阶段并发任务的协调

它通过维护一个计数器来工作,每完成一个操作就减一,当计数器归零时释放所有等待的线程。

问题2:CountDownLatch与CyclicBarrier的区别?

答:主要区别有:

  1. 重用性:CountDownLatch是一次性的,计数器归零后不能重置;CyclicBarrier可以通过reset()方法重置,可重复使用。

  2. 计数方向:CountDownLatch计数器只能减少;CyclicBarrier的计数器是先减少,后自动重置。

  3. 触发方式:CountDownLatch是调用countDown()方法;CyclicBarrier是调用await()方法。

  4. 使用场景:CountDownLatch适用于一个或多个线程等待其他操作完成;CyclicBarrier适用于多个线程互相等待至某个状态,然后一起继续运行。

6.2 代码实现题

问题:实现一个高并发限流器,限制最大并发请求数为100,当请求处理完成后允许新的请求进入。

答:可以使用Semaphore实现最基本的限流功能,但结合CountDownLatch可以实现更加灵活的控制:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;public class RequestLimiter {// 最大并发数限制private final Semaphore semaphore;// 已完成的请求计数private final AtomicInteger completedRequests = new AtomicInteger(0);// 用于等待所有请求处理完成private CountDownLatch completionLatch;public RequestLimiter(int maxConcurrent) {this.semaphore = new Semaphore(maxConcurrent);}// 开始一轮请求处理public void startBatch(int requestCount) {this.completionLatch = new CountDownLatch(requestCount);this.completedRequests.set(0);}// 处理请求public void processRequest(Runnable task) throws InterruptedException {// 获取许可semaphore.acquire();try {// 执行请求处理task.run();} finally {// 释放许可semaphore.release();// 完成一个请求completedRequests.incrementAndGet();// 计数器减一completionLatch.countDown();}}// 等待所有请求处理完成public void awaitCompletion() throws InterruptedException {completionLatch.await();}// 获取已完成的请求数public int getCompletedRequestCount() {return completedRequests.get();}public static void main(String[] args) throws InterruptedException {RequestLimiter limiter = new RequestLimiter(10);int totalRequests = 100;// 开始处理100个请求limiter.startBatch(totalRequests);// 提交请求for (int i = 0; i < totalRequests; i++) {final int requestId = i;new Thread(() -> {try {System.out.println("请求 " + requestId + " 等待处理");limiter.processRequest(() -> {try {// 模拟请求处理System.out.println("处理请求 " + requestId);Thread.sleep((long) (Math.random() * 500));} catch (InterruptedException e) {Thread.currentThread().interrupt();}});System.out.println("请求 " + requestId + " 处理完成");} catch (InterruptedException e) {Thread.currentThread().interrupt();}}).start();}// 等待所有请求处理完成limiter.awaitCompletion();System.out.println("所有请求处理完成,共处理: " + limiter.getCompletedRequestCount() + " 个请求");}
}

6.3 原理分析题

问题:CountDownLatch的内部实现原理是什么?它是如何实现线程等待和唤醒的?

答:CountDownLatch内部基于AQS(AbstractQueuedSynchronizer)框架实现:

  1. 状态管理:使用AQS的state变量作为计数器

  2. 等待机制:调用await()时,如果计数器不为0,则当前线程会被加入到AQS的等待队列中

  3. 唤醒机制:调用countDown()时,计数器减1,当减至0时,AQS会释放所有等待的线程

  4. 线程安全性:通过CAS(Compare And Swap)操作保证计数器更新的原子性

具体实现上,CountDownLatch包含一个继承自AQS的内部类Sync,它重写了tryAcquireSharedtryReleaseShared方法:

  • tryAcquireShared:只有当计数器为0时才返回1,表示获取成功。
  • tryReleaseShared:使用CAS操作安全地减少计数器,并在计数器变为0时返回true

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/79275.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/79275.shtml
英文地址,请注明出处:http://en.pswp.cn/web/79275.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux 内核链表宏的详细解释

&#x1f527; Linux 内核链表结构概览 Linux 内核中的链表结构定义在头文件 <linux/list.h> 中。核心结构是&#xff1a; struct list_head {struct list_head *next, *prev; }; 它表示一个双向循环链表的节点。链表的所有操作都围绕这个结构体展开。 &#x1f9e9; …

分书问题的递归枚举算法

分数问题的递归枚举算法 一、问题引入二、解题步骤1.问题分析思维导图2.解题步骤 三、代码实现1.代码2.复杂度分析 四、个人总结 一、问题引入 分书问题是指&#xff1a;已知 n 个人对 m 本书的喜好&#xff08;n≤m&#xff09;&#xff0c;现要将 m 本书分给 n 个人&#xf…

密码学--AES

一、实验目的 1、完成AES算法中1轮加密和解密操作 2、掌握AES的4个基本处理步骤 3、理解对称加密算法的“对称”思想 二、实验内容 1、题目内容描述 &#xff08;1&#xff09;利用C语言实现字节代换和逆向字节代换&#xff0c;字节查S盒代换 &#xff08;2&#xff09;利…

【工具记录分享】提取bilibili视频字幕

F12大法 教程很多 但方法比较统一 例快速提取视频字幕&#xff01;适用B站、AI字幕等等。好用 - 哔哩哔哩 无脑小工具 哔哩哔哩B站字幕下载_在线字幕解析-飞鱼视频下载助手 把链接扔进去就会自动生成srt文件 需要txt可以配合&#xff1a; SRT转为TXT

使用fdisk 、gdisk管理分区

用 fdisk 管理分区 fdisk 命令工具默认将磁盘划分为 mbr 格式的分区 命令&#xff1a; fdisk 设备名 fdisk 命令以交互方式进行操作的&#xff0c;在菜单中选择相应功能键即可 [rootlocalhost ~]# fdisk /dev/sda # 对 sda 进行分区 Command (m for help): # 进入 fdis…

【Linux基础】程序和软件安装管理命令

目录 install命令 which命令 install命令 作用&#xff1a;它是用于安装或复制文件到指定位置&#xff0c;并且可以同时设置文件的权限、所有者和所属组等属性。它通常用于脚本中&#xff0c;用于自动化安装程序或配置文件的部署。 基本用法&#xff1a; install [选项] 源…

C++模板梳理

目录 函数模板 类模板 变量模板 模板全特化 模板偏特化 模板显式实例化解决文件分离问题 折叠表达式 模板的二阶段编译 待决名(dependent name) SFINAE 概念与约束 函数模板 函数模板不是函数&#xff0c;只有实例化的函数模板&#xff0c;编译器才能生成实际的函数…

数据链共享:从印巴空战到工业控制的跨越性应用

摘要 本文通过对印巴空战中数据链共享发挥关键作用的分析&#xff0c;引出数据链共享在工业控制领域同样具有重大价值的观点。深入阐述 DIOS 工业控制操作系统作为工业数据链共享基础技术的特点、架构及应用优势&#xff0c;对比空战场景与工业控制场景下数据链共享的相…

巡检机器人数据处理技术的创新与实践

摘要 随着科技的飞速发展&#xff0c;巡检机器人在各行业中逐渐取代人工巡检&#xff0c;展现出高效、精准、安全等显著优势。当前&#xff0c;巡检机器人已从单纯的数据采集阶段迈向对采集数据进行深度分析的新阶段。本文探讨了巡检机器人替代人工巡检的现状及优势&#xff0c…

在 Flink + Kafka 实时数仓中,如何确保端到端的 Exactly-Once

在 Flink Kafka 构建实时数仓时&#xff0c;确保端到端的 Exactly-Once&#xff08;精确一次&#xff09; 需要从 数据消费&#xff08;Source&#xff09;、处理&#xff08;Processing&#xff09;、写入&#xff08;Sink&#xff09; 三个阶段协同设计&#xff0c;结合 Fli…

当可视化遇上 CesiumJS:突破传统,打造前沿生产配套方案

CesiumJS 技术基础介绍 CesiumJS 是一款基于 JavaScript 的开源库&#xff0c;专门用于创建动态、交互式的地理空间可视化。它利用 WebGL 技术&#xff0c;能够在网页浏览器中流畅地渲染高分辨率的三维地球和地图场景。CesiumJS 支持多种地理空间数据格式&#xff0c;包括但不…

RabbitMQ深入学习

继续上一节的学习&#xff0c;上一节学习了RabbitMQ的基本内容&#xff0c;本节学习RabbitMQ的高级特性。 RocketMQ的高级特性学习见这篇博客 目录 1.消息可靠性1.1生产者消息确认1.2消息持久化1.3消费者消息确认1.4消费失败重试机制1.5消息可靠性保证总结 2.什么是死信交换机…

Linux系统:虚拟文件系统与文件缓冲区(语言级内核级)

本节重点 初步理解一切皆文件理解文件缓冲区的分类用户级文件缓冲区与内核级文件缓冲区用户级文件缓冲区的刷新机制两级缓冲区的分层协作 一、虚拟文件系统 1.1 理解“一切皆文件” 我们都知道操作系统访问不同的外部设备&#xff08;显示器、磁盘、键盘、鼠标、网卡&#…

在c++中老是碰到string,这是什么意思?

定义一个string类型变量的引用&#xff0c;相当于给现有变量起个别名&#xff0c;与指针还是不一样的。比如string a;string& ba;这两句&#xff0c;b与a实际上是一回事&#xff0c;表示的是同一块内存。 std是系统的一个命名空间(有关命名空间可以参阅namespace_百度百科)…

Day21 奇异值分解(SVD)全面解析

一、奇异值分解概述 奇异值分解是线性代数中一个重要的矩阵分解方法&#xff0c;对于任何矩阵&#xff0c;无论是结构化数据转化成的“样本 * 特征”矩阵&#xff0c;还是天然以矩阵形式存在的图像数据&#xff0c;都能进行等价的奇异值分解&#xff08;SVD&#xff09;。 二…

akshare爬虫限制,pywencai频繁升级个人做量化,稳定数据源和券商的选择

做量化&#xff0c;数据和交易接口是策略和自动化交易的基石&#xff0c;而稳定的数据和快人一步的交易接口是个人做量化的催化剂。 之前写过一篇文章&#xff1a;个人做量化常用的数据&#xff0c;多以爬虫为主&#xff0c;最近akshare爬虫限制&#xff0c;pywencai频繁升级。…

数字签名与证书

1. 数字签名与证书 摘要算法用来实现完整性&#xff0c;能够为数据生成独一无二的“指纹”&#xff0c;常用的算法是 SHA-2&#xff1b;数字签名是私钥对摘要的加密&#xff0c;可以由公钥解密后验证&#xff0c;实现身份认证和不可否认&#xff1b;公钥的分发需要使用数字证书…

Ubuntu22.04安装显卡驱动/卸载显卡驱动

报错 今日输入nvidia-smi报错,在安装了535和550,包括560都没办法解决,但是又怕乱搞导致环境损坏,打算把显卡卸载然后重新安装系统默认推荐版本的显卡驱动 qinqin:~$ nvidia-smi Failed to initialize NVML: Driver/library version mismatch NVML library version: 560.35卸载…

Web 架构之负载均衡全解析

文章目录 一、引言二、思维导图三、负载均衡的定义与作用定义作用1. 提高可用性2. 增强性能3. 实现扩展性 四、负载均衡类型硬件负载均衡代表设备优缺点 软件负载均衡应用层负载均衡代表软件优缺点 网络层负载均衡代表软件优缺点 五、负载均衡算法轮询算法&#xff08;Round Ro…

linux下的Redis的编译安装与配置

配合做开发经常会用到redis&#xff0c;整理下编译安装配置过程&#xff0c;仅供参考&#xff01; --------------------------------------Redis的安装与配置-------------------------------------- 下载 wget https://download.redis.io/releases/redis-6.2.6.tar.gz tar…