并发 - JUC 三大辅助同步工具

并发编程 —— JUC 三大辅助同步工具

除了锁和原子类,JUC 还提供了一些非常有用的“同步辅助”工具,用于协调多个线程之间的复杂协作。其中最常用的有三个:CountDownLatch, CyclicBarrier, 和 Semaphore。


1. CountDownLatch (倒计时门闩)

  • 核心思想: 允许一个或多个线程等待其他一组线程完成操作后再继续执行。它像一个倒数计数器。

  • 工作流程:

    1. 创建一个 CountDownLatch 并初始化一个计数值(比如 N)。
    2. 一个或多个线程调用 latch.await() 方法,这些线程会被阻塞,进入等待状态。
    3. 其他 N 个线程执行各自的任务,每个任务完成后,调用一次 latch.countDown() 方法,将计数器减 1。
    4. 当计数器减到 0 时,所有在 await() 方法上等待的线程都会被唤醒,继续执行。
  • 特点: 计数器只能使用一次,减到 0 后就不能再重置了。

  • 底层实现:

    • 基于 AQS (AbstractQueuedSynchronizer) 实现,利用了 AQS 的共享模式。
    • 构造时传入的计数值 N 被用作 AQS 的 state 状态。
    • await() 方法会检查 state 是否为 0,如果不是,则线程进入 AQS 的等待队列并挂起。
    • countDown() 方法会以 CAS 的方式安全地将 state 减 1。当 state 被减到 0 时,它会唤醒所有在等待队列中的线程。
  • 使用场景:

    • 主线程等待子任务完成:例如,一个主线程需要启动多个服务(每个服务在一个独立的线程中启动),并且必须等待所有服务都成功启动后才能对外提供服务。
    • 并行计算与结果汇总:将一个大任务拆分成多个子任务并行执行,主线程使用 await() 等待所有子任务完成后,再对结果进行汇总和处理。
      • 当你的场景是“一个线程(或少数几个线程)等待其他所有线程完成某个任务后,再继续执行”时。例如,等待多个初始化服务全部启动、等待多个文件下载完成、等待多个子任务计算完毕
  • 生活比喻: 就像火箭发射的倒计时。

    • latch.await(): 总指挥官(主线程)下达了“等待”命令,他盯着倒计时屏幕,在倒数结束前绝不按下发射按钮。
    • latch.countDown(): 每个负责检查燃料、电路、天气等岗位的工程师(子线程),完成自己的检查后,就向指挥中心报告“我已完成”,相当于让倒计时减 1。
    • 计数归零: 当所有工程师都报告完毕,倒计时变为 0,总指挥官立即按下发射按钮。

2. CyclicBarrier (循环栅栏)

  • 核心思想: 让一组线程互相等待,直到所有线程都到达一个公共的“栅栏点”(Barrier Point),然后再一起继续执行。

  • 工作流程:

    1. 创建一个 CyclicBarrier,并指定需要等待的线程数量(parties)。
    2. 每个线程在完成自己的一部分工作后,调用 barrier.await() 方法,告诉栅栏“我到了”。
    3. 调用 await() 的线程会被阻塞。
    4. 直到最后一个线程也调用了 await(),栅栏被“冲破”,所有被阻塞的线程会同时被唤醒,继续执行后续任务。
  • 特点: 可以循环使用。当所有线程被释放后,栅栏可以被重置,用于下一轮的等待。

  • 底层实现:

    • 基于 ReentrantLock 和 Condition 实现。
    • 每个线程调用 await() 时,会先获取锁,然后将一个计数器 count 减 1。
    • 如果 count 不为 0,则线程在 Condition 上 await() 进入等待状态。
    • 如果 count 为 0(表示最后一个线程已到达),该线程会唤醒 Condition 上所有等待的线程 (signalAll()),并重置栅栏以便下次使用。
  • 使用场景:

    • 多线程数据分片处理:在进行大规模数据计算时,可以将数据分成多片,每个线程处理一片。当所有线程都处理完当前阶段后(到达栅栏),再统一进入下一阶段(例如数据合并、结果交换)。
    • 模拟并发测试:需要模拟大量用户在同一时刻发起请求的场景。可以让多个线程准备好请求数据,然后在栅栏处等待,当所有线程都准备好后,同时发起请求,以达到瞬时高并发的效果。
      • 当你的场景是“一组线程需要达到一个共同的同步点,然后才能一起继续执行下一个阶段的任务”时。这种多发生在需要分阶段处理、且每个阶段所有参与者都必须同步的场景。例
        如,多线程计算结果的阶段性汇总、模拟高并发测试的统一启动点。
  • 生活比喻: 就像组织一场春游。

    • 约定好早上 8 点在公园门口(栅栏点)集合,人齐了才出发。
    • 每个同学(线程)从自己家里出发,到达公园门口后,就调用 await(),表示“我到了,我开始等其他人”。
    • 当最后一个同学也到达门口时,班长(栅栏)大喊一声“人齐了,出发!”,于是所有同学一起涌入公园开始游玩。
    • 玩完第一个景点后,大家可以再约定到第二个景点门口集合,再次使用这个“栅栏”。

3. Semaphore (信号量)

  • 核心思想: 用于控制同时访问某个特定资源的线程数量。它内部维护了一组“许可证”。

  • 工作流程:

    1. 创建一个 Semaphore 并初始化许可证的数量。
    2. 线程在访问资源前,必须先调用 semaphore.acquire() 方法获取一个许可证。如果许可证已发完,线程就会被阻塞。
    3. 线程访问完资源后,必须调用 semaphore.release() 方法归还许可证。
  • 特点: 主要用于流量控制和资源限制。

  • 底层实现:

    • 同样基于 AQS 实现,许可证的数量就是 AQS 的 state。
    • acquire() 方法会尝试以 CAS 方式将 state 减 1。如果 state 已经为 0,则获取失败,线程进入 AQS 等待队列。
    • release() 方法会将 state 加 1,并唤醒等待队列中的一个线程。
    • Semaphore 也支持公平和非公平两种模式。
  • 使用场景:

    • 资源池管理:例如数据库连接池。如果连接池中只有 10 个连接,就可以创建一个 Semaphore(10),每个线程在获取连接前先 acquire(),用完后 release(),从而保证最多只有 10 个线程能同时持有连接。
    • 接口流量控制:限制对某个第三方 API 或服务的并发调用次数,以防止因请求过多而被封禁或产生高额费用。
    • 控制内存密集型操作:如果某个操作非常消耗内存,可以通过信号量限制同时执行该操作的线程数,防止系统内存溢出。
  • 生活比喻: 就像一个停车场的入口。

    • Semaphore(3): 表示这个停车场总共只有 3 个车位(许可证)。
    • acquire(): 每当有车想进入停车场,就必须在入口的闸机处取一张停车卡(获取许可证)。如果卡发完了,后面的车就只能在入口外排队等着。
    • release(): 每当有车离开停车场,它会把停车卡还给闸机(归还许可证),这样排队的下一辆车就可以进来了。

总结对比

工具 核心作用 是否可重用 底层基础 计数器 生活比喻
CountDownLatch 一个或多个线程等其他线程 否 AQS (共享) countDown() 减计数 火箭发射倒计时
CyclicBarrier 一组线程互相等 是 Lock & Condition await() 减计数 春游集合等人
Semaphore 控制并发数量 是 AQS 停车场车位

posted @ 2026-01-21 16:01  我是刘瘦瘦  阅读(42)  评论(0)    收藏  举报