Java并发工具类详解:Semaphore、CyclicBarrier与CountDownLatch

Java并发工具类详解:Semaphore、CyclicBarrier与CountDownLatch

在多线程编程中,线程间的同步与协作是核心难题。Java并发包(java.util.concurrent)提供了多种工具类,简化了复杂场景下的线程协调逻辑。本文将深入解析三个常用工具类------SemaphoreCyclicBarrierCountDownLatch,通过场景案例、代码实现和对比分析,理清它们的适用场景与核心区别。

一、Semaphore:控制并发访问的"信号灯"

1. 核心功能

Semaphore(信号量)用于限制同时访问某个资源的线程数量,类似"限流"机制。它维护一组"许可"(permits),线程需先获取许可才能访问资源,访问完成后释放许可供其他线程使用。

2. 适用场景

  • 控制并发请求数(如接口限流)
  • 管理资源池访问(如数据库连接池、线程池)
  • 实现生产者-消费者模型中的缓冲区边界控制

3. 代码案例:数据库连接池限流

假设数据库最多支持3个并发连接,超过则需等待:

java 复制代码
import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 初始化3个许可(最多3个线程同时访问)
        Semaphore semaphore = new Semaphore(3);
        System.out.println("=== 数据库连接池初始化完成,最大并发连接数:3 ===");
        
        // 模拟10个线程竞争访问数据库
        for (int i = 0; i < 10; i++) {
            final int threadId = i + 1;
            new Thread(() -> {
                try {
                    System.out.println("线程" + threadId + "尝试获取数据库连接...");
                    // 获取许可(若满则阻塞等待)
                    semaphore.acquire();
                    System.out.println("线程" + threadId + "✅ 获取到数据库连接,开始执行SQL操作...");
                    Thread.sleep(1000); // 模拟数据库操作耗时
                    System.out.println("线程" + threadId + "📝 SQL操作执行完成");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放许可(必须在finally中执行,避免资源泄漏)
                    semaphore.release();
                    System.out.println("线程" + threadId + "🔄 释放数据库连接,当前可用连接数:" + semaphore.availablePermits());
                }
            }).start();
            // 错开线程启动时间,便于观察输出
            Thread.sleep(200);
        }
    }
}

输出示例

复制代码
=== 数据库连接池初始化完成,最大并发连接数:3 ===
线程1尝试获取数据库连接...
线程1✅ 获取到数据库连接,开始执行SQL操作...
线程2尝试获取数据库连接...
线程2✅ 获取到数据库连接,开始执行SQL操作...
线程3尝试获取数据库连接...
线程3✅ 获取到数据库连接,开始执行SQL操作...
线程4尝试获取数据库连接...
线程1📝 SQL操作执行完成
线程1🔄 释放数据库连接,当前可用连接数:1
线程4✅ 获取到数据库连接,开始执行SQL操作...
线程5尝试获取数据库连接...
线程2📝 SQL操作执行完成
线程2🔄 释放数据库连接,当前可用连接数:1
线程5✅ 获取到数据库连接,开始执行SQL操作...
...(后续线程按同样逻辑执行)

二、CyclicBarrier:多线程协同的"循环屏障"

1. 核心功能

CyclicBarrier(循环屏障)让多个线程相互等待 ,直到所有线程都到达某个"屏障点"后,再一起继续执行。与其他工具类不同,它支持重复使用(通过reset()重置屏障)。

2. 适用场景

  • 多阶段任务(如"需求分析→编码→测试"的开发流程)
  • 数据分片处理(各线程处理完分片后,等待汇总结果)
  • 模拟并发场景(所有线程就绪后同时执行)

3. 代码案例:多阶段开发流程

3个开发者需依次完成三个阶段任务,每个阶段必须全员完成后才能进入下一阶段:

java 复制代码
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        int developerCount = 3; // 3个开发者线程
        System.out.println("=== 项目启动,共" + developerCount + "名开发者参与 ===");
        
        // 屏障触发时执行的任务(可选,如阶段总结)
        CyclicBarrier barrier = new CyclicBarrier(developerCount, () -> {
            System.out.println("======================================");
            System.out.println("📢 所有开发者已完成当前阶段,准备进入下一阶段!");
            System.out.println("======================================");
        });

        for (int i = 0; i < developerCount; i++) {
            final int developerId = i + 1;
            new Thread(() -> {
                try {
                    // 第一阶段:需求分析
                    System.out.println("开发者" + developerId + ":开始需求分析(阶段1)");
                    Thread.sleep((long) (Math.random() * 1000) + 500); // 模拟不同耗时
                    System.out.println("开发者" + developerId + ":需求分析完成(阶段1),等待其他人...");
                    barrier.await(); // 等待其他开发者

                    // 第二阶段:编码
                    System.out.println("开发者" + developerId + ":开始编码开发(阶段2)");
                    Thread.sleep((long) (Math.random() * 1000) + 500);
                    System.out.println("开发者" + developerId + ":编码开发完成(阶段2),等待其他人...");
                    barrier.await();

                    // 第三阶段:测试
                    System.out.println("开发者" + developerId + ":开始功能测试(阶段3)");
                    Thread.sleep((long) (Math.random() * 1000) + 500);
                    System.out.println("开发者" + developerId + ":功能测试完成(阶段3),等待其他人...");
                    barrier.await();

                    System.out.println("开发者" + developerId + ":🎉 所有阶段任务完成!");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

输出示例

复制代码
=== 项目启动,共3名开发者参与 ===
开发者1:开始需求分析(阶段1)
开发者2:开始需求分析(阶段1)
开发者3:开始需求分析(阶段1)
开发者2:需求分析完成(阶段1),等待其他人...
开发者1:需求分析完成(阶段1),等待其他人...
开发者3:需求分析完成(阶段1),等待其他人...
======================================
📢 所有开发者已完成当前阶段,准备进入下一阶段!
======================================
开发者1:开始编码开发(阶段2)
开发者2:开始编码开发(阶段2)
开发者3:开始编码开发(阶段2)
开发者3:编码开发完成(阶段2),等待其他人...
开发者1:编码开发完成(阶段2),等待其他人...
开发者2:编码开发完成(阶段2),等待其他人...
======================================
📢 所有开发者已完成当前阶段,准备进入下一阶段!
======================================
...(第三阶段流程类似)

三、CountDownLatch:等待前置任务的"倒计时器"

1. 核心功能

CountDownLatch(倒计时器)让一个或多个线程等待其他线程完成指定操作 后再执行。它维护一个计数器,线程完成任务后调用countDown()递减计数器,当计数器为0时,所有等待的线程被唤醒。计数器不可重置,为一次性工具

2. 适用场景

  • 主线程等待子线程初始化完成(如服务器启动前的准备工作)
  • 汇总多线程任务结果(如多个线程计算部分结果,主线程汇总)
  • 模拟并发测试(等待所有线程就绪后同时执行)

3. 代码案例:服务器启动前的初始化

服务器启动前,需等待5个初始化任务(如加载配置、连接数据库)完成:

java 复制代码
import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        int taskCount = 5; // 5个初始化任务
        CountDownLatch latch = new CountDownLatch(taskCount);
        System.out.println("=== 服务器启动流程开始,需完成" + taskCount + "项初始化任务 ===");

        // 启动初始化任务
        for (int i = 0; i < taskCount; i++) {
            final int taskId = i + 1;
            new Thread(() -> {
                try {
                    System.out.println("初始化任务" + taskId + ":启动(当前剩余任务数:" + latch.getCount() + ")");
                    // 模拟不同任务耗时(1-2秒)
                    Thread.sleep((long) (Math.random() * 1000) + 1000);
                    System.out.println("初始化任务" + taskId + ":执行成功");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown(); // 任务完成,计数器-1
                    System.out.println("初始化任务" + taskId + ":已标记完成(剩余任务数:" + latch.getCount() + ")");
                }
            }).start();
        }

        // 主线程等待所有初始化完成
        System.out.println("=== 主线程等待所有初始化任务完成... ===");
        latch.await(); // 阻塞直到计数器为0
        System.out.println("=== 所有初始化任务完成,服务器启动成功!🎉 ===");
    }
}

输出示例

复制代码
=== 服务器启动流程开始,需完成5项初始化任务 ===
初始化任务1:启动(当前剩余任务数:5)
初始化任务2:启动(当前剩余任务数:5)
初始化任务3:启动(当前剩余任务数:5)
初始化任务4:启动(当前剩余任务数:5)
初始化任务5:启动(当前剩余任务数:5)
=== 主线程等待所有初始化任务完成... ===
初始化任务3:执行成功
初始化任务3:已标记完成(剩余任务数:4)
初始化任务1:执行成功
初始化任务1:已标记完成(剩余任务数:3)
初始化任务5:执行成功
初始化任务5:已标记完成(剩余任务数:2)
初始化任务2:执行成功
初始化任务2:已标记完成(剩余任务数:1)
初始化任务4:执行成功
初始化任务4:已标记完成(剩余任务数:0)
=== 所有初始化任务完成,服务器启动成功!🎉 ===

四、三者核心区别对比

特性 Semaphore CyclicBarrier CountDownLatch
核心功能 控制并发访问的线程数量(限流) 多线程相互等待,一起继续执行 等待其他线程完成后再执行
计数器/许可 许可数量(可动态调整) 参与线程数(可通过reset()重置) 等待的任务数(一次性递减)
阻塞对象 申请许可的线程(争夺资源的线程) 所有参与的线程(相互等待) 调用await()的线程(等待者)
复用性 可重复使用(许可释放后可再次获取) 可重复使用(reset()重置屏障) 一次性(计数器到0后无法重置)
典型场景 限流(连接池、并发请求控制) 多阶段协同任务(分步骤流程) 等待前置任务完成(初始化、汇总)
核心方法 acquire()/release() await()/reset() countDown()/await()

五、总结

  • Semaphore 是"门卫",通过许可机制控制同时访问资源的线程数,适合限流场景(如连接池、接口并发控制)。
  • CyclicBarrier 是"集合点",让多个线程到达屏障后一起执行下一阶段,适合多步骤协同任务(如分阶段开发流程)。
  • CountDownLatch 是"发令枪",等待所有前置任务完成后触发后续操作,适合初始化、结果汇总等场景。

这三个工具类均基于AQS(AbstractQueuedSynchronizer)实现,但设计目标不同。实际开发中需根据具体同步需求选择:如需控制并发量用Semaphore,如需多线程协同用CyclicBarrier,如需等待前置任务用CountDownLatch

相关推荐
鼠鼠我捏,要死了捏5 小时前
深度解析JVM GC调优实践指南
java·jvm·gc
IT·陈寒5 小时前
当 JVM 开始“内卷”:一次性能优化引发的 GC 战争
java·jvm·性能优化
不会吃萝卜的兔子6 小时前
spring微服务宏观概念
java·spring·微服务
麦麦鸡腿堡6 小时前
Java的抽象类
java·开发语言
Java水解6 小时前
Go基础:Go语言中 Goroutine 和 Channel 的声明与使用
java·后端·面试
人间乄惊鸿客6 小时前
python-day8
开发语言·python
Chan166 小时前
流量安全优化:基于 Nacos 和 BloomFilter 实现动态IP黑名单过滤
java·spring boot·后端·spring·nacos·idea·bloomfilter
傻童:CPU6 小时前
C语言需要掌握的基础知识点之递归
c语言·开发语言
黑金IT6 小时前
PHP 后台通过权限精制飞书多维表格
开发语言·php·飞书