JUC 工具类大全:CountDownLatch、Semaphore、CyclicBarrier 有何区别?

在 Java 并发编程中,JUC(java.util.concurrent)包提供了多个“同步协作”工具类,其中 CountDownLatchSemaphoreCyclicBarrier 是最常见但也最易混淆的三个。

今天我们用源码视角 + 使用场景,全面讲清它们的机制与差异。


一、CountDownLatch:一次性倒计数协作器

应用场景

主线程等待多个子任务完成再继续,或模拟多线程并发启动(如压测工具)。

使用方式

CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // 执行任务
        latch.countDown();
    }).start();
}
latch.await(); // 主线程阻塞直到计数为 0

底层原理

  • 内部基于 AQS(AbstractQueuedSynchronizer)

  • 初始状态设为计数值

  • 每次 countDown() 会对 state 减 1,等到为 0 时释放所有等待线程

特点总结

特性

是否可重置

❌ 一次性

是否可共享

✅ 多个线程可同时等待

适用场景

等待某些事件完成再继续


二、Semaphore:计数信号量限流器

应用场景

控制访问资源的线程数量,典型场景如:数据库连接池、限流、并发下载。

使用方式

Semaphore semaphore = new Semaphore(2); // 最多2个线程同时访问
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        try {
            semaphore.acquire();
            // 临界区逻辑
        } finally {
            semaphore.release();
        }
    }).start();
}

底层原理

  • 同样基于 AQS

  • 支持 公平非公平 策略

  • acquire() 尝试获取许可;无可用则阻塞

  • release() 释放许可并唤醒阻塞线程

特点总结

特性

是否可重用

✅ 是

是否限并发

✅ 控制线程并发数

是否公平可配

✅ 构造函数可指定


三、CyclicBarrier:可重用的屏障器

应用场景

多线程并发执行阶段性任务后,需要统一等待,再进入下一阶段。例如分布式计算中的 barrier 同步。

使用方式

CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程准备完毕,继续执行...");
});
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // 阶段性处理
        barrier.await(); // 等待所有线程到达
        // 下一阶段
    }).start();
}

底层原理

  • 内部依赖 ReentrantLock + Condition

  • 每次 await() 会让线程阻塞,直到达到设定数量后,统一唤醒

  • 唤醒逻辑执行完后会自动 复位,可用于下一轮同步

特点总结

特性

是否可重置

✅ 是

回调机制

✅ 到达屏障可触发回调任务

用于多轮协作

✅ 支持循环使用


四、三者对比一览表

特性/类名

CountDownLatch

Semaphore

CyclicBarrier

线程等待方数量

多个线程等待

限制并发数

等待线程达到阈值

是否可复用

内部实现机制

AQS

AQS

Lock + Condition

是否支持回调

是(barrierAction)

使用侧重点

协作通知

并发限流

阻塞同步点


五、实践建议与误区提醒

  • 如果你只需要“等 N 个线程完成”,用 CountDownLatch

  • 如果你要“限制并发线程数”,用 Semaphore

  • 如果你想要“统一起点再出发”,并且支持多轮,选 CyclicBarrier

  • CountDownLatch 是一次性的,用完就不能再用!

  • Semaphore 使用后记得 release(),否则可能导致线程永久阻塞

  • CyclicBarrier 回调(barrierAction)运行在最后一个到达的线程中,需注意回调逻辑中不要出错


六、源码深挖:CountDownLatch 的 AQS 使用方式详解

核心类与状态字段

public class CountDownLatch {
    private static final class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            setState(count);
        }
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int c = getState();
                if (c == 0) return false;
                int next = c - 1;
                if (compareAndSetState(c, next))
                    return next == 0;
            }
        }
    }
}

关键要点解析

  • tryAcquireShared 判断是否可以获取共享锁,只有 state == 0 时才返回成功。

  • tryReleaseShared 实现了原子减计数,并判断是否触发唤醒。

  • 底层等待队列由 AQS 管理,因此支持大量并发线程安全等待。


七、Semaphore 限流原理与非公平策略优化探讨

公平 vs 非公平实现

  • 公平模式下:等待队列中的线程严格按照 FIFO 顺序获取许可。

  • 非公平模式:线程尝试立即获取许可,提高吞吐,但可能出现“饥饿”。

static final class NonfairSync extends Sync {
    protected int tryAcquireShared(int acquires) {
        for (;;) {
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 || compareAndSetState(available, remaining))
                return remaining;
        }
    }
}

应用建议

  • 高并发场景倾向使用 非公平模式 提高吞吐(如 RPC 限流)。

  • 业务侧需考虑低优先级线程可能长时间得不到资源。


八、CyclicBarrier 高并发下的 bug 处理技巧

典型异常:BrokenBarrierException

当一个线程被中断或超时,会导致其他线程都抛出 BrokenBarrierException,此时 barrier 被破坏,需要重建。

try {
    barrier.await(2, TimeUnit.SECONDS);
} catch (TimeoutException | BrokenBarrierException e) {
    // 自定义重置逻辑
    barrier.reset();
}

如何避免?

  • 为每个阶段任务设置合理的超时时间,避免某个线程卡死影响整体。

  • 利用 isBroken() 和 reset() 方法动态修复 Barrier。


九、实战进阶:如何在高并发业务中选择合适的 JUC 工具类?

业务场景

推荐工具类

说明

等待多个线程初始化完成

CountDownLatch

主线程可通过 await() 等待所有子任务完成

控制并发线程数量(限流)

Semaphore

使用 acquire()/release() 控制资源使用

多线程分阶段处理任务

CyclicBarrier

每个阶段线程同步,自动重置,可用于多轮任务

线程等待另一个线程的结果

CompletableFuture

更适合线程间依赖而非数量同步

分布式节点等待启动

Zookeeper Barrier

若需跨 JVM 同步,可使用分布式协调工具


十、性能测试:CountDownLatch vs CyclicBarrier 多线程压测对比

测试目标

在 1000 个线程同时并发任务场景下:

  • 使用 CountDownLatch 作为任务完成信号

  • 使用 CyclicBarrier 进行任务开始统一控制

结论总结

  • CountDownLatch 更轻量,适用于一次性任务通知

  • CyclicBarrier 更适合阶段性调度,但维护成本更高

  • 过度使用同步工具类,反而会引入性能瓶颈,应谨慎设计并发模型


十一、最佳实践建议

  • 工具类之间不能混用,语义不同,场景匹配是关键

  • 若需复用协调机制,优先选 CyclicBarrier,避免重复创建 CountDownLatch。

  • 使用 JMH 工具测试并发同步性能,量化方案差异。

你可能感兴趣的:(多线程,java,开发语言)