Java线程协作工具:CountDownLatch 、CyclicBarrier、Phaser、Semaphore 、Exchanger

在高并发系统中,多个线程之间的协作是不可避免的。Java 从 JDK 1.5 开始引入了 java.util.concurrent(JUC)包,其中包含了一系列强大的线程协作工具类,极大简化了并发编程的复杂性。

CountDownLatch(JDK 5+):倒计时门闩

使用场景

CountDownLatch 常用于"一个或多个线程等待其他线程完成任务后再继续执行"的场景,例如:

  • 主线程等待所有子线程初始化完毕再启动服务。
  • 并发测试中模拟 N 个请求同时发出后统计总耗时。
  • 资源加载完成后统一触发后续逻辑。

核心 API

csharp 复制代码
public CountDownLatch(int count);        // 初始化计数器
public void await() throws InterruptedException;          // 阻塞等待计数归零
public boolean await(long timeout, TimeUnit unit);       // 带超时的等待
public void countDown();                                 // 计数减一

使用示例

csharp 复制代码
public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 5;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 正在执行任务...");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown(); // 任务完成,计数减一
                }
            }).start();
        }

        latch.await(); // 主线程等待所有任务完成
        System.out.println("所有任务已完成,主线程继续执行!");
    }
}

输出:

erlang 复制代码
Thread-0 正在执行任务...
Thread-1 正在执行任务...
Thread-3 正在执行任务...
Thread-2 正在执行任务...
Thread-4 正在执行任务...
所有任务已完成,主线程继续执行!

应用场景示例

服务启动检查:

scss 复制代码
// 等待数据库、缓存、消息队列初始化完成
CountDownLatch startupLatch = new CountDownLatch(3);
initDB(startupLatch);
initCache(startupLatch);
initMQ(startupLatch);
startupLatch.await();
startHttpServer();

使用注意点

  • 不可重用 :一旦计数归零,无法再次使用(除非新建实例)。需要重复使用的场景应考虑 CyclicBarrier
  • 若某个线程未调用 countDown(),会导致主线程永久阻塞, 导致死锁,所以务必在 finally 块中调用。
  • await() 可被中断,需处理 InterruptedException

底层原理

CountDownLatch 基于 AQS(AbstractQueuedSynchronizer) 实现:(AQS 原理详见Java并发编程:Lock原理详解

  • 内部状态 state 表示剩余计数值。
  • await() 对应 AQS 的 acquireSharedInterruptibly(1),尝试获取共享锁,若 state != 0 则入队阻塞。
  • countDown() 调用 releaseShared(1),将 state 减 1,当 state == 0 时唤醒所有等待线程。

CyclicBarrier(JDK 5+):循环屏障

使用场景

CyclicBarrier 适用于"多个线程互相等待,全部到达某一点后才能继续"的场景,例如:

  • 多线程分阶段计算(每阶段结束后汇总结果)。
  • 游戏开发中等待所有玩家准备就绪。
  • 并行算法中的同步点。

核心 API

csharp 复制代码
public CyclicBarrier(int parties); 
public CyclicBarrier(int parties, Runnable barrierAction); // 所有线程到达后执行的动作
public int await() throws InterruptedException, BrokenBarrierException;
public int await(long timeout, TimeUnit unit) throws ...;
public void reset(); // 重置屏障(可能中断当前等待)

使用示例

csharp 复制代码
public class CyclicBarrierExample {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("所有线程已到达屏障,执行汇总操作!");
        });

        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 第一阶段完成");
                    barrier.await(); // 等待其他线程

                    System.out.println(Thread.currentThread().getName() + " 第二阶段开始");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

输出:

复制代码
Thread-1 第一阶段完成
Thread-2 第一阶段完成
Thread-0 第一阶段完成
所有线程已到达屏障,执行汇总操作!
Thread-0 第二阶段开始
Thread-1 第二阶段开始
Thread-2 第二阶段开始

应用场景示例

多轮并行计算:

ini 复制代码
CyclicBarrier barrier = new CyclicBarrier(4, this::aggregateResults);
for (int i = 0; i < 4; i++) {
    executor.submit(() -> {
        while (round < MAX_ROUNDS) {
            computePartialResult();
            barrier.await(); // 同步进入下一轮
            round++;
        }
    });
}

底层原理

CyclicBarrier 不基于 AQS,而是使用 ReentrantLock + Condition 实现:(Condition 原理详见Java并发编程:Lock原理详解

  • 每次调用 await(),线程进入条件队列等待。
  • 当最后一个线程到达时,唤醒所有等待线程,并执行 barrierAction(如有)。
  • 完成后自动重置计数器,支持重复使用

若某个线程在等待期间被中断或超时,屏障将进入"损坏"(broken)状态,后续调用会抛出 BrokenBarrierException

CyclicBarrier 与 CountDownLatch 的区别

特性 CountDownLatch CyclicBarrier
控制方向 一方等待多方 所有线程互相等待
是否可重用
是否支持回调 ✅(barrierAction)
底层实现 AQS ReentrantLock + Condition

Phaser(JDK 7+):灵活的阶段同步器

Phaser 新增于 JDK 7,是对 CyclicBarrierCountDownLatch 的强大扩展,支持动态注册参与者、多阶段同步和分层结构,适用于更复杂的并发协调场景。

使用场景

  • 多阶段并行任务(如 Map-Reduce 的 map → shuffle → reduce)。
  • 动态增减工作线程(例如任务中途加入新 worker)。
  • 大规模并行计算中需要分组同步(通过分层 Phaser 实现)。
  • 替代多个 CyclicBarrierCountDownLatch 的组合逻辑。

核心 API

csharp 复制代码
// 构造方法
public Phaser();                     // 无初始参与者
public Phaser(int parties);          // 指定初始参与者数量
public Phaser(Phaser parent);        // 构建父子分层结构

// 核心方法
public int register();               // 注册一个新参与者,返回当前阶段号
public int arrive();                 // 到达屏障,不等待
public int arriveAndAwaitAdvance();  // 到达并等待其他参与者
public int arriveAndDeregister();    // 到达并注销自己(退出后续阶段)
public boolean isTerminated();       // 是否已终止(当参与者数为0时终止)

使用示例

动态多阶段任务:

scss 复制代码
public class PhaserExample {
    public static void main(String[] args) {
        Phaser phaser = new Phaser(0); // 初始无参与者

        // 启动3个任务线程
        for (int i = 0; i < 3; i++) {
            phaser.register(); // 注册参与者
            new Thread(() -> {
                try {
                    for (int phase = 0; phase < 3; phase++) {
                        System.out.println(Thread.currentThread().getName() 
                            + " 完成阶段 " + phase);
                        // 到达屏障并等待其他线程
                        phaser.arriveAndAwaitAdvance();
                    }
                    // 任务结束,注销自己
                    phaser.arriveAndDeregister();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }

        // 主线程等待所有阶段完成
        while (!phaser.isTerminated()) {
            Thread.yield();
        }
        System.out.println("所有阶段已完成!");
    }
}

输出:

复制代码
Thread-1 完成阶段 0
Thread-2 完成阶段 0
Thread-0 完成阶段 0
Thread-0 完成阶段 1
Thread-2 完成阶段 1
Thread-1 完成阶段 1
Thread-2 完成阶段 2
Thread-1 完成阶段 2
Thread-0 完成阶段 2
所有阶段已完成!

高级特性:分层 Phaser

当参与者数量极大(如数千线程)时,单一 Phaser 的同步开销会很高。此时可构建树状分层结构

scss 复制代码
Phaser root = new Phaser();
Phaser group1 = new Phaser(root); // 子 Phaser,父为 root
Phaser group2 = new Phaser(root);

// group1 内部线程只与同组同步,最终由 root 汇总
group1.register();
new Thread(() -> {
    group1.arriveAndAwaitAdvance(); // 仅与 group1 同步
}).start();

这种结构能显著减少锁竞争,提升大规模并发性能。、

应用场景示例

分阶段并行处理:

scss 复制代码
// 模拟三阶段数据处理:加载 → 转换 → 输出
Phaser phaser = new Phaser(0);

List<Thread> workers = IntStream.range(0, 4)
    .mapToObj(i -> {
        phaser.register();
        return new Thread(() -> {
            load();   phaser.arriveAndAwaitAdvance();
            transform(); phaser.arriveAndAwaitAdvance();
            output(); phaser.arriveAndAwaitAdvance();
            phaser.arriveAndDeregister();
        });
    })
    .peek(Thread::start)
    .collect(Collectors.toList());

// 等待全部完成
while (!phaser.isTerminated()) {
    Thread.yield();
}

底层原理

不同于 CountDownLatchSemaphorePhaser 未基于 AQS ,而是自行实现同步机制,它的实现融合了 自旋锁 + 队列管理

  • 状态字段 :使用一个 long 类型的 state 字段,高位存阶段号(phase),低位存参与者数量(parties)。
  • 自适应等待策略
    • 参与者少时使用 自旋等待(低延迟)。
    • 参与者多或等待时间长时,自动切换到 阻塞队列(节省 CPU)。

Phaser 与 CyclicBarrier 的对比

特性 CyclicBarrier Phaser
引入版本 JDK 1.5 JDK 1.7
是否可重用 ✅(自动重置) ✅(阶段递增)
动态增减参与者 ✅(register() / arriveAndDeregister())
分层支持 ✅(父子结构)
阶段号追踪 ✅(getPhase())
终止机制 损坏即不可用 参与者归零后优雅终止
性能(大规模) 较差(全局锁) 更优(分层 + 自适应等待)

使用建议:

  • 若参与者数量固定且较少,用 CyclicBarrier 即可。
  • 若需动态调整、多阶段、大规模,则优先选择 Phaser

Semaphore(JDK 5+):信号量

使用场景

Semaphore 用于控制同时访问特定资源的线程数量,比如:

  • 数据库连接池(最多 N 个连接)。
  • 接口限流(如每秒最多处理 100 个请求)。
  • 模拟有限资源的分配(如停车场车位)。
  • new Semaphore(1) 实际上等价于 synchronizedReentrantLock

核心 API

java 复制代码
public Semaphore(int permits);
public Semaphore(int permits, boolean fair); // 公平/非公平模式
public void acquire() throws InterruptedException;
public void release();
public boolean tryAcquire(); // 非阻塞尝试获取

使用示例

scss 复制代码
public class SemaphoreExample {
    private static final Semaphore semaphore = new Semaphore(2); // 最多2个线程并发

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 获取许可,正在执行...");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + " 释放许可");
                }
            }).start();
        }
    }
}

输出:

erlang 复制代码
Thread-0 获取许可,正在执行...
Thread-1 获取许可,正在执行...
Thread-0 释放许可
Thread-2 获取许可,正在执行...
Thread-1 释放许可
Thread-3 获取许可,正在执行...
Thread-2 释放许可
Thread-3 释放许可
Thread-4 获取许可,正在执行...
Thread-4 释放许可

应用场景示例

API 限流:

java 复制代码
private final Semaphore rateLimiter = new Semaphore(100); // 每秒最多100请求

public void handleRequest() {
    if (rateLimiter.tryAcquire()) {
        try {
            process();
        } finally {
            rateLimiter.release();
        }
    } else {
        throw new RuntimeException("请求过于频繁");
    }
}

底层原理

同样基于 AQS :(AQS 原理详见Java并发编程:Lock原理详解

  • state 表示可用许可数量。
  • acquire() 尝试减少 state,若不足则入队等待。
  • release() 增加 state 并唤醒等待线程。
  • 支持公平模式(按请求顺序分配许可)。

Exchanger(JDK 5+):数据交换器

使用场景

Exchanger 专为两个线程之间交换数据而设计,典型场景如:

  • 双缓冲机制(一个线程写入缓冲区 A,另一个写入 B,满后交换)。
  • 生产者与消费者高效传递中间结果。
  • 日志收集器中交换日志缓冲区。

核心 API

java 复制代码
public V exchange(V x) throws InterruptedException;
public V exchange(V x, long timeout, TimeUnit unit) throws ...;

示例代码

ini 复制代码
public class ExchangerExample {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();

        new Thread(() -> {
            try {
                String data = "Thread-A 的数据";
                System.out.println("A 准备交换: " + data);
                String received = exchanger.exchange(data);
                System.out.println("A 收到: " + received);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(1000); // 模拟延迟
                String data = "Thread-B 的数据";
                System.out.println("B 准备交换: " + data);
                String received = exchanger.exchange(data);
                System.out.println("B 收到: " + received);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
    }
}

输出:

less 复制代码
A 准备交换: Thread-A 的数据
B 准备交换: Thread-B 的数据
B 收到: Thread-A 的数据
A 收到: Thread-B 的数据

应用场景示例

双缓冲日志:

ini 复制代码
Exchanger<List<LogEntry>> exchanger = new Exchanger<>();
List<LogEntry> bufferA = new ArrayList<>(1000);
List<LogEntry> bufferB = new ArrayList<>(1000);

// 写线程不断填充 bufferA
// 写满后与读线程交换,读线程异步刷盘

底层原理

Exchanger 内部使用 Slot 或 Node 结构 暂存数据,通过 CAS + 自旋 实现高效交换:

  • 第一个调用 exchange() 的线程将数据放入槽位并等待。
  • 第二个线程到来后,取出对方数据,放入自己的数据,完成交换。
  • 若超时或中断,会清理槽位并抛出异常。

注意 :仅支持两个线程配对交换 ,第三个线程调用 exchange() 会一直阻塞直到有新的配对。

工具类对比总结

特性 CountDownLatch CyclicBarrier Phaser Semaphore Exchanger
引入版本 1.5 1.5 1.7 1.5 1.5
是否可重用
协作模式 一方等多方 所有互相等 多阶段动态同步 控制并发数 两线程交换
动态参与者
分层支持
底层机制 AQS ReentrantLock+Condition 自定义同步(CAS+自旋+队列) AQS CAS+自旋
典型用途 初始化等待 分阶段计算 复杂并行任务协调 限流 数据交换
相关推荐
无尽的沉默6 小时前
SpringBoot整合Redis
spring boot·redis·后端
摸鱼的春哥6 小时前
春哥的Agent通关秘籍07:5分钟实现文件归类助手【实战】
前端·javascript·后端
Victor3567 小时前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack7 小时前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo7 小时前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
Victor3567 小时前
MongoDB(3)什么是文档(Document)?
后端
牛奔9 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌14 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
KYGALYX15 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了15 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结