从入门到入土,轻度解析Java多线程三大同步工具的使用案例

78 阅读5分钟
  1. CountDownLatch(一次性屏障):一次性屏障,主线程等待N个子任务完成
  2. CyclicBarrier(可重用屏障):可重复使用,所有线程到达屏障后继续执行
  3. Semaphore(信号量模型):用于控制并发线程数,如数据库连接池限制

CountDownLatch

工作原理:

  • 初始化时设定计数器
  • 每个工作线程完成任务后调用countDown()减少计数
  • 主线程通过await()阻塞等待,直到计数器归零

适用场景:

  1. 服务启动依赖(如需要先启动数据库再启动主应用)
  2. 并行计算汇总(多个计算任务完成后合并结果)
  3. 压力测试(等待所有测试线程就绪后同时发起请求)

注意事项:

  1. 计数器不可重置,如果需要重复使用应选择CyclicBarrier
  2. await()可以设置超时时间避免无限等待
  3. 建议将countDown()放在finally块确保执行
  4. 避免在异步回调方法之外操作计数器

与Thread.join()的区别:

  • 更灵活:不需要直接控制线程实例
  • 更精细:可以控制多个不同类型的任务
  • 更智能:支持超时机制和状态查询

性能特点:

  • 基于AQS实现,await()会触发线程排队
  • 适用于低频次同步场景(初始化、阶段检查等)
  • 不适合高频同步操作(考虑用Phaser替代)

以服务启动时,需要首先预热依赖的其他服务为例


/**
 * @author 反卷但卷
 * @version 1.0
 * @description CountDownLatch的应用 Demo
 */
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;

public class ServerClusterBootstrapper {

    // 模拟服务器节点启动流程
    public static void main(String[] args) throws InterruptedException {
        // 需要等待3个核心服务启动
        final CountDownLatch serviceLatch = new CountDownLatch(3);

        new Thread(() -> startDatabase(serviceLatch), "DB-Thread").start();
        new Thread(() -> startCache(serviceLatch), "Cache-Thread").start();
        new Thread(() -> startMQ(serviceLatch), "MQ-Thread").start();

        System.out.println("主线程开始等待基础服务初始化...");
        long startTime = System.currentTimeMillis();

        serviceLatch.await();

        System.out.printf("所有基础服务就绪,总耗时%dms\n启动主应用...\n",
                System.currentTimeMillis() - startTime);
    }

    // 模拟数据库服务启动
    private static void startDatabase(CountDownLatch latch) {
        try {
            int delay = ThreadLocalRandom.current().nextInt(800, 1200);
            Thread.sleep(delay);
            System.out.println(Thread.currentThread().getName()
                    + " 数据库连接建立完成 (" + delay + "ms)");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            latch.countDown();
        }
    }

    // 模拟缓存服务启动
    private static void startCache(CountDownLatch latch) {
        try {
            int delay = ThreadLocalRandom.current().nextInt(500, 1000);
            Thread.sleep(delay);
            System.out.println(Thread.currentThread().getName()
                    + " 缓存预热完成 (" + delay + "ms)");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            latch.countDown();
        }
    }

    // 模拟消息队列服务启动
    private static void startMQ(CountDownLatch latch) {
        try {
            int delay = ThreadLocalRandom.current().nextInt(300, 1500);
            Thread.sleep(delay);
            System.out.println(Thread.currentThread().getName()
                    + " 消息队列通道创建成功 (" + delay + "ms)");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            latch.countDown();
        }
    }
}

CyclicBarrier

运作原理:

  • 初始化时指定参与线程数和屏障动作(Runnable)
  • 每个线程调用await()后进入等待
  • 当全部线程到达屏障点时,自动执行屏障动作
  • 计数器自动重置,可重复使用

典型使用场景:

  1. 多阶段计算(如ETL流程的分阶段执行)
  2. 迭代运算(每轮迭代需要同步状态)
  3. 离散事件模拟(如游戏帧同步)
  4. 压力测试中的波浪式请求

关键注意事项:

  1. await()方法返回每个线程的到达索引(可用于领导选举)
  2. 重置机制:reset()方法会强制进入broken状态
  3. 如果一个线程在等待中失败,其他线程会收到BrokenBarrierException
  4. 屏障动作在最后一个线程到达后由任意线程执行

与CountDownLatch的对比:

  • 重用性:CyclicBarrier可重复触发,CountDownLatch一次性
  • 等待方向:CyclicBarrier是多线程互相等待,CountDownLatch是主线程等待工作线程
  • 扩展功能:CyclicBarrier支持屏障动作和到达状态查询

以大批量数据分片并行处理为例


/**
 * @author 反卷但卷
 * @version 1.0
 * @description CyclicBarrier的使用 Demo
 */
import java.util.concurrent.*;

public class DataProcessingPipeline {
    // 模拟3个数据分片并行处理
    private static final int WORKER_COUNT = 3;

    public static void main(String[] args) {
        // 设置3个阶段的屏障,每个阶段结束后执行统计操作
        CyclicBarrier barrier = new CyclicBarrier(WORKER_COUNT, () -> {
            System.out.printf("阶段完成,当前时间:%s%n", System.currentTimeMillis());
        });

        ExecutorService executor = Executors.newFixedThreadPool(WORKER_COUNT);

        for (int i = 1; i <= WORKER_COUNT; i++) {
            final int partitionId = i;
            executor.submit(() -> processDataPartition(partitionId, barrier));
        }

        executor.shutdown();
    }

    private static void processDataPartition(int partitionId, CyclicBarrier barrier) {
        try {
            // 第一阶段:数据加载
            stageWork("加载", partitionId, 200);
            barrier.await();

            // 第二阶段:数据清洗
            stageWork("清洗", partitionId, 500);
            barrier.await();

            // 第三阶段:数据持久化
            stageWork("保存", partitionId, 300);
            barrier.await();

            System.out.printf("分片%d 全部流程完成%n", partitionId);
        } catch (InterruptedException | BrokenBarrierException e) {
            Thread.currentThread().interrupt();
            System.err.println("处理过程被中断");
        }
    }

    private static void stageWork(String stageName, int partitionId, int baseTime)
            throws InterruptedException {
        int delay = ThreadLocalRandom.current().nextInt(baseTime, baseTime + 200);
        Thread.sleep(delay);
        System.out.printf("分片%d %s完成 (耗时%dms)%n",
                partitionId, stageName, delay);
    }
}

Semaphore

工作流程:

  • 初始化时指定资源总量(许可证数量)
  • 请求资源:acquire() 或 tryAcquire()
  • 使用资源:执行临界区代码
  • 释放资源:release()

适用场景:

  1. 连接池/对象池资源管理
  2. 接口限流(特别是突发流量控制)
  3. 生产者消费者模式(有界缓冲区)
  4. 并行计算任务调度

关键方法对比:

方法是否阻塞支持超时立即返回
acquire()××
acquireUninterruptibly()××
tryAcquire()××
tryAcquire(timeout)×

注意事项:

  1. 释放许可数量可以超过初始值(允许动态调整池大小)
  2. 建议在finally块中释放许可
  3. 使用公平模式会降低吞吐量但保证公平性
  4. 单个线程可以多次获取许可(需相应次数释放)
  5. 监控指标:availablePermits()、getQueueLength()

以控制连接池连接数量为例


/**
 * @author 反卷但卷
 * @version 1.0
 * @description Semaphore的使用 Demo,信号量
 */
import java.util.concurrent.*;

public class DatabaseConnectionPool {
    private static final int POOL_SIZE = 5;
    private static final Semaphore availableConnections = new Semaphore(POOL_SIZE, true);
    private static final int TOTAL_THREADS = 10;

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(TOTAL_THREADS);

        // 模拟突发流量:10个并发请求
        for (int i = 1; i <= TOTAL_THREADS; i++) {
            final int taskId = i;
            executor.execute(() -> {
                try {
                    queryDatabase(taskId);
                } catch (InterruptedException e) {
                    System.err.println("任务" + taskId + "被中断");
                    Thread.currentThread().interrupt();
                }
            });
        }

        executor.shutdown();
    }

    private static void queryDatabase(int taskId) throws InterruptedException {
        System.out.printf("任务%d 等待连接... (可用许可:%d)%n",
                taskId, availableConnections.availablePermits());

        // 设置等待超时时间
        if (!availableConnections.tryAcquire(3, TimeUnit.SECONDS)) {
            System.err.println("任务" + taskId + " 等待超时,拒绝服务");
            return;
        }

        try (ConnectionProxy connection = new ConnectionProxy(taskId)) {
            System.out.printf("任务%d 获取连接 (剩余许可:%d)%n",
                    taskId, availableConnections.availablePermits());
            // 模拟SQL执行时间
            Thread.sleep(ThreadLocalRandom.current().nextInt(500, 1500));
            System.out.println("任务" + taskId + " 查询完成");
        } catch (Exception e) {
            System.err.println("任务" + taskId + " 执行异常:" + e.getMessage());
        } finally {
            availableConnections.release();
            System.out.printf("任务%d 释放连接 (当前可用:%d)%n",
                    taskId, availableConnections.availablePermits());
        }
    }

    // 模拟数据库连接资源
    private static class ConnectionProxy implements AutoCloseable {
        private final int taskId;

        public ConnectionProxy(int taskId) {
            this.taskId = taskId;
        }

        @Override
        public void close() {
            // 实际项目应包含连接回收逻辑
        }
    }
}