Java CountDownLatch闭锁完全指南:从概念到源码

Java CountDownLatch闭锁完全指南:从概念到源码

Java CountDownLatch闭锁完全指南:从概念到源码

    • 一、🔴 CountDownLatch概述:什么是闭锁?
      • 1.1 🟠 官方定义
      • 1.2 🟡 核心特性
    • 二、🔵 流程图:CountDownLatch核心工作流程
    • 三、🟣 CountDownLatch核心方法详解
      • 3.1 🟤 构造方法
      • 3.2 🟠 await() —— 等待计数器归零
      • 3.3 🟡 countDown() —— 计数器减1
      • 3.4 🟣 其他常用方法
    • 四、🟠 流程图:CountDownLatch的两种经典用法
      • 4.1 🟤 用法一:启动信号(一次触发,多线程同时启动)
      • 4.2 🟠 用法二:完成信号(等待所有任务完成)
      • 4.3 🟡 CountDownLatch vs Thread.join()
    • 五、🟤 底层原理:AQS共享模式
      • 5.1 🔵 核心架构
      • 5.2 🟠 await() 的底层流程
      • 5.3 🟣 countDown() 的底层流程
    • 六、🟢 避坑指南
      • 6.1 🔴 常见陷阱
      • 6.2 🟢 最佳实践清单

🌺The Begin🌺点点关注,收藏不迷路🌺

⬇ ⬇ 底部 ⬇ ⬇

📌本文导读:本文将系统讲解Java中CountDownLatch(闭锁)的核心概念、工作原理和使用方法,从"倒计时门闩"的生动比喻到AQS的底层实现,并通过大量实战示例帮助你掌握这个经典的并发同步工具。全文包含五大核心章节3个彩色流程图9个代码示例。预计阅读时间25分钟


一、🔴 CountDownLatch概述:什么是闭锁?

1.1 🟠 官方定义

根据Oracle官方文档的定义,CountDownLatch是一个同步辅助工具,它允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。

形象比喻CountDownLatch就像一道倒计时门闩(Latch)。门闩上有一个计数器,初始值为N。每当一个线程完成了自己的任务,就拨动一次门闩(计数器减1)。当计数器归零时,门闩"啪"的一声打开,所有被挡在门外的等待线程一拥而入。

1.2 🟡 核心特性

特性说明
计数器不可重置一旦计数器归零,就永久保持开放状态,无法重置
一次性使用这是一个"one-shot"现象——计数不能被重置
等待灵活调用countDown()的线程不需要等待计数器归零就可以继续执行
底层基于AQS使用AQS的共享模式实现

📌与CyclicBarrier的核心区别CountDownLatch是"一次性门闩",CyclicBarrier是"循环栅栏"。前者计数器只减不增,后者可重用。


二、🔵 流程图:CountDownLatch核心工作流程

📋 创建CountDownLatch
计数器 = N

🟢 线程A调用 await
计数器>0, 进入阻塞

🟠 线程1完成任务
调用 countDown

🟡 计数器减1
当前值 = N-1

计数器 == 0?

⏳ 等待线程继续阻塞

🔴 门闩打开

🟣 唤醒所有
等待中的线程

🔵 所有等待线程
继续执行

🟠 线程2完成任务
调用 countDown


三、🟣 CountDownLatch核心方法详解

3.1 🟤 构造方法

// 🔴 初始化计数器为3,表示需要等待3个操作完成CountDownLatchlatch=newCountDownLatch(3);

参数说明count表示需要等待的操作数量。若count小于0,构造方法会抛出IllegalArgumentException

3.2 🟠 await() —— 等待计数器归零

作用:使当前线程阻塞,直到计数器归零。如果计数器已经为零,则立即返回,不会阻塞。

// 🟢 主线程等待try{latch.await();// 阻塞,直到计数器归零}catch(InterruptedExceptione){Thread.currentThread().interrupt();}

带超时的版本

// 🟡 最多等待5秒,超时则继续执行if(latch.await(5,TimeUnit.SECONDS)){System.out.println("所有任务已完成");}else{System.out.println("等待超时,部分任务未完成");}

3.3 🟡 countDown() —— 计数器减1

作用:将计数器减1。当计数器减到0时,释放所有等待线程。

// 🔵 每个子线程完成任务后调用latch.countDown();

⚠️重要特性:调用countDown()的线程不阻塞,会立即继续执行。一个线程可以在不同阶段多次调用countDown()

3.4 🟣 其他常用方法

方法说明
getCount()返回当前计数器的值
await(long timeout, TimeUnit unit)带超时的等待
countDown()减少计数器

四、🟠 流程图:CountDownLatch的两种经典用法

🔵 用法2: 主线程等待工作线程完成

主线程: 创建 latch=N

启动N个工作线程

每个线程完成任务后
调用 countDown

所有N个任务
都完成?

主线程: await返回
继续执行

🟢 用法1: 工作线程等待启动信号

主线程: 创建 latch=1

工作线程调用 await
全部阻塞

主线程完成准备工作

主线程: latch.countDown

所有工作线程同时启动

4.1 🟤 用法一:启动信号(一次触发,多线程同时启动)

根据Oracle官方文档中的示例,CountDownLatch初始化为1时可以作为一个**“启动门闩”**:所有工作线程调用await()等待,直到主线程调用countDown()打开门闩。

// 🔴 场景:所有运动员等待发令枪响publicclassStartSignalDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{intN=5;CountDownLatchstartSignal=newCountDownLatch(1);// 发令枪// 🟢 创建N个运动员(工作线程)for(inti=0;i<N;i++){newThread(()->{System.out.println(Thread.currentThread().getName()+" 已就位,等待发令...");try{startSignal.await();// 等待发令枪响System.out.println(Thread.currentThread().getName()+" 🏃 起跑!");}catch(InterruptedExceptione){Thread.currentThread().interrupt();}},"运动员-"+(i+1)).start();}Thread.sleep(3000);// 模拟准备时间System.out.println("🔴 预备... 砰!");startSignal.countDown();// 发令枪响,所有线程同时启动}}

4.2 🟠 用法二:完成信号(等待所有任务完成)

根据Oracle官方文档的示例,CountDownLatch初始化为N可以用于等待N个任务全部完成

// 🟡 场景:主线程等待所有玩家加载完成publicclassCompleteSignalDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{intplayerCount=10;CountDownLatchdoneSignal=newCountDownLatch(playerCount);// 需要等待10个玩家// 🔵 创建10个玩家线程for(inti=0;i<playerCount;i++){newThread(()->{// 模拟加载过程intprogress=0;while(progress<100){progress+=newRandom().nextInt(20);if(progress>100)progress=100;System.out.println(Thread.currentThread().getName()+" 加载: "+progress+"%");try{Thread.sleep(100);}catch(InterruptedExceptione){}}System.out.println("✅ "+Thread.currentThread().getName()+" 加载完成!");doneSignal.countDown();// 🟣 计数器减1},"玩家-"+(i+1)).start();}// 🔴 主线程等待所有玩家加载完成doneSignal.await();System.out.println("🎮 所有玩家已加载完成,游戏开始!");}}

4.3 🟡 CountDownLatch vs Thread.join()

根据源码分析,CountDownLatch相比Thread.join()有两个重要优势:

对比维度Thread.join()CountDownLatch
等待条件等待线程终止等待计数器归零
灵活性必须持有线程引用countDown()可在任何地方调用
线程池支持无法用于线程池✅ 完美配合线程池使用
// 🔵 配合线程池使用ExecutorServiceexecutor=Executors.newFixedThreadPool(3);CountDownLatchlatch=newCountDownLatch(3);for(inti=0;i<3;i++){executor.submit(()->{// 执行任务latch.countDown();// 无需持有线程引用});}latch.await();// 主线程阻塞executor.shutdown();

五、🟤 底层原理:AQS共享模式

5.1 🔵 核心架构

CountDownLatch内部通过一个继承自AbstractQueuedSynchronizer(AQS)的内部类Sync来实现同步控制。所有对计数器的操作都转交给Sync实例处理。

CountDownLatch

内部类 Sync
继承 AQS

state 字段
存储计数器值

tryAcquireShared
检查 state==0

tryReleaseShared
CAS减1

5.2 🟠 await() 的底层流程

当调用await()时:

  1. 转交给Sync处理
  2. 检查state(计数器)是否等于0
  3. 如果state == 0直接返回(门已开)
  4. 如果state > 0,当前线程进入AQS同步队列阻塞等待

5.3 🟣 countDown() 的底层流程

当调用countDown()时:

  1. 尝试通过CASstate减1
  2. 如果减1后state == 0,调用releaseShared()释放所有等待线程
  3. AQS共享模式下,releaseShared()级联唤醒同步队列中的所有阻塞线程

📌内存可见性保证:根据官方文档,在计数归零之前,一个线程中调用countDown()之前的操作,happen-before另一个线程从await()成功返回之后的操作。


六、🟢 避坑指南

6.1 🔴 常见陷阱

序号陷阱正确做法
计数器与任务数不匹配确保countDown()调用次数等于初始计数器
未处理InterruptedException捕获后恢复中断状态或向上抛出
误以为可以重置计数器CountDownLatch不可重置,如需重用用CyclicBarrier
finally中忘记countDown()即使在异常情况下也要确保计数器减少

6.2 🟢 最佳实践清单

序号实践建议说明
countDown()放在finally确保异常时计数器也能归零
使用await(timeout)防止永久阻塞避免线程无限等待
初始计数器与任务数量保持一致否则死锁或过早触发
需要重置时使用CyclicBarrierCountDownLatch是一次性工具


🌺The End🌺点点关注,收藏不迷路🌺

⬆ ⬆ 顶部 ⬆ ⬆