CountDownLatch详解与项目实战

CountDownLatch是Java并发编程中的一个重要同步工具类,它允许一个或多个线程等待其他线程完成操作后再继续执行。本文将全面解析CountDownLatch的原理、核心方法、使用场景,并通过实际项目案例展示其应用。

一、CountDownLatch核心原理

CountDownLatch是基于"倒计时门闩"机制实现的同步辅助类,其核心工作原理如下:

  1. 计数器机制:创建时指定一个正整数作为计数器初始值,代表需要等待完成的事件或任务数量
  2. 等待机制 :调用await()方法的线程会被阻塞,直到计数器减到零
  3. 递减机制 :其他线程完成任务后调用countDown()方法使计数器减一
  4. 唤醒机制:当计数器减至零时,所有等待的线程会被唤醒

CountDownLatch底层基于AQS(AbstractQueuedSynchronizer)框架实现,通过CAS操作保证计数器递减的线程安全性。其state变量使用volatile修饰,确保多线程间的可见性。

重要特性​:

  • 一次性使用:计数器不可重置,归零后无法重复使用
  • 非独占锁:所有线程共享计数器状态,无锁竞争问题
  • 支持超时:提供带超时参数的await方法,避免无限等待

二、核心API与方法

CountDownLatch提供了简洁而强大的API:

  1. 构造方法
java 复制代码
public CountDownLatch(int count)

初始化计数器,count必须为非负整数。若为0,await()会立即返回

  1. await()​
java 复制代码
public void await() throws InterruptedException

使当前线程等待,直到计数器减至零或线程被中断

  1. await(long timeout, TimeUnit unit)​
java 复制代码
public boolean await(long timeout, TimeUnit unit) throws InterruptedException

带超时的等待,在指定时间内计数器未归零则返回false

  1. countDown()​
csharp 复制代码
public void countDown()

将计数器减一,当减至零时唤醒所有等待线程

  1. getCount()​
csharp 复制代码
public long getCount()

获取当前计数器的值(较少使用)

三、典型应用场景

1. 主线程等待子任务完成

最常见的场景是主线程需要等待多个子线程完成任务后再继续执行。例如系统启动时需要等待多个组件初始化完成。

代码示例​:

csharp 复制代码
public class MainThreadWaitDemo {
    public static void main(String[] args) throws InterruptedException {
        final int THREAD_COUNT = 5;
        CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        
        for (int i = 0; i < THREAD_COUNT; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 开始执行任务");
                    Thread.sleep((long) (Math.random() * 1000));
                    System.out.println(Thread.currentThread().getName() + " 任务完成");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown(); // 确保计数器减少
                }
            }, "Worker-" + i).start();
        }
        
        System.out.println("主线程等待所有子线程完成任务...");
        latch.await();
        System.out.println("所有子线程任务已完成,主线程继续执行!");
    }
}

2. 并行任务处理与结果合并

将大任务拆分为多个子任务并行执行,最后合并结果。

代码示例​:

ini 复制代码
public class ParallelCalculationDemo {
    public long calculateSum(int[] array) throws InterruptedException {
        int threadCount = 4;
        CountDownLatch latch = new CountDownLatch(threadCount);
        AtomicLong totalSum = new AtomicLong(0);
        
        int chunkSize = array.length / threadCount;
        for (int i = 0; i < threadCount; i++) {
            final int start = i * chunkSize;
            final int end = (i == threadCount - 1) ? array.length : (i + 1) * chunkSize;
            
            new Thread(() -> {
                long partialSum = 0;
                for (int j = start; j < end; j++) {
                    partialSum += array[j];
                }
                totalSum.addAndGet(partialSum);
                System.out.println("线程计算范围[" + start + "," + end + "),结果:" + partialSum);
                latch.countDown();
            }).start();
        }
        
        latch.await();
        return totalSum.get();
    }
}

3. 模拟高并发测试

使用CountDownLatch实现所有线程同时开始执行,模拟高并发场景。

代码示例​:

csharp 复制代码
public class ConcurrentTest {
    private static final int THREAD_COUNT = 100;
    private static CountDownLatch startLatch = new CountDownLatch(1);
    private static CountDownLatch endLatch = new CountDownLatch(THREAD_COUNT);

    public static void main(String[] args) throws InterruptedException {
        // 创建100个测试线程
        for (int i = 0; i < THREAD_COUNT; i++) {
            new Thread(() -> {
                try {
                    // 等待开始信号
                    startLatch.await();
                    // 执行测试逻辑
                    performTest();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    endLatch.countDown();
                }
            }).start();
        }
        
        // 所有线程准备就绪后,发出开始信号
        System.out.println("所有线程准备就绪,开始测试...");
        startLatch.countDown();
        
        // 等待所有测试线程完成
        endLatch.await();
        System.out.println("所有测试线程执行完毕");
    }
}

4. 服务启动依赖检查

在分布式系统中,确保所有依赖服务都启动完成后再启动主服务。

代码示例​:

arduino 复制代码
public class ServiceStartupCheck {
    public static void main(String[] args) throws InterruptedException {
        int numberOfServices = 3;
        CountDownLatch latch = new CountDownLatch(numberOfServices);
        
        for (int i = 0; i < numberOfServices; i++) {
            new Thread(new Service(latch, "Service-" + (i + 1))).start();
        }
        
        latch.await(); // 等待所有服务启动
        System.out.println("所有服务已启动,系统启动完成。");
    }
}

class Service implements Runnable {
    private final CountDownLatch latch;
    private final String serviceName;
    
    Service(CountDownLatch latch, String serviceName) {
        this.latch = latch;
        this.serviceName = serviceName;
    }
    
    @Override
    public void run() {
        try {
            System.out.println(serviceName + " 正在启动...");
            Thread.sleep((long) (Math.random() * 3000)); // 模拟不同启动时间
            System.out.println(serviceName + " 启动完成。");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            latch.countDown();
        }
    }
}

四、项目实战案例

案例1:电商订单处理系统

在电商系统中,处理订单时需要并行执行库存检查、用户验证和支付验证,全部通过后才能完成订单处理。

实现代码​:

csharp 复制代码
public class OrderProcessor {
    public void processOrder(String orderId) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        
        // 库存检查
        new Thread(() -> {
            try {
                System.out.println("开始库存检查...");
                Thread.sleep(1000);
                System.out.println("库存检查完成");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                latch.countDown();
            }
        }).start();
        
        // 用户验证
        new Thread(() -> {
            try {
                System.out.println("开始用户验证...");
                Thread.sleep(1500);
                System.out.println("用户验证完成");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                latch.countDown();
            }
        }).start();
        
        // 支付验证
        new Thread(() -> {
            try {
                System.out.println("开始支付验证...");
                Thread.sleep(800);
                System.out.println("支付验证完成");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                latch.countDown();
            }
        }).start();
        
        System.out.println("等待所有验证完成...");
        latch.await();
        System.out.println("订单处理完成: " + orderId);
    }
}

案例2:游戏服务器启动初始化

游戏服务器启动时需要并行初始化多个组件(数据库、缓存、网络模块等),全部初始化成功后才能开放服务。

实现代码​:

csharp 复制代码
public class GameServerInitializer {
    public boolean initialize() {
        System.out.println("开始游戏服务器初始化...");
        CountDownLatch latch = new CountDownLatch(4);
        AtomicBoolean success = new AtomicBoolean(true);
        
        // 数据库初始化
        new Thread(() -> {
            try {
                System.out.println("初始化数据库连接...");
                Thread.sleep(2000);
                System.out.println("数据库初始化完成");
            } catch (InterruptedException e) {
                success.set(false);
            } finally {
                latch.countDown();
            }
        }).start();
        
        // Redis初始化
        new Thread(() -> {
            try {
                System.out.println("初始化Redis连接...");
                Thread.sleep(1000);
                System.out.println("Redis初始化完成");
            } catch (InterruptedException e) {
                success.set(false);
            } finally {
                latch.countDown();
            }
        }).start();
        
        // 网络模块初始化
        new Thread(() -> {
            try {
                System.out.println("初始化网络模块...");
                Thread.sleep(1500);
                System.out.println("网络模块初始化完成");
            } catch (InterruptedException e) {
                success.set(false);
            } finally {
                latch.countDown();
            }
        }).start();
        
        // 配置加载
        new Thread(() -> {
            try {
                System.out.println("加载游戏配置...");
                Thread.sleep(800);
                System.out.println("配置加载完成");
            } catch (InterruptedException e) {
                success.set(false);
            } finally {
                latch.countDown();
            }
        }).start();
        
        try {
            boolean finished = latch.await(10, TimeUnit.SECONDS);
            if (finished && success.get()) {
                System.out.println("游戏服务器初始化成功!");
                return true;
            } else {
                System.out.println("游戏服务器初始化失败!");
                return false;
            }
        } catch (InterruptedException e) {
            System.out.println("初始化被中断");
            return false;
        }
    }
}

案例3:大数据处理中的分片计算

在大数据处理场景中,将大数据集分割为多个分片并行处理,最后汇总结果。

实现代码​:

scss 复制代码
public class BigDataProcessor {
    public Result processBigData(DataSet dataSet) throws InterruptedException {
        int availableProcessors = Runtime.getRuntime().availableProcessors();
        CountDownLatch latch = new CountDownLatch(availableProcessors);
        List<DataChunk> chunks = dataSet.split(availableProcessors);
        ConcurrentMap<Integer, PartialResult> partialResults = new ConcurrentHashMap<>();
        
        for (int i = 0; i < availableProcessors; i++) {
            final int chunkIndex = i;
            new Thread(() -> {
                try {
                    PartialResult result = processChunk(chunks.get(chunkIndex));
                    partialResults.put(chunkIndex, result);
                } finally {
                    latch.countDown();
                }
            }).start();
        }
        
        latch.await();
        return mergeResults(partialResults);
    }
    
    private PartialResult processChunk(DataChunk chunk) {
        // 处理数据分片的实际逻辑
        return new PartialResult();
    }
    
    private Result mergeResults(ConcurrentMap<Integer, PartialResult> partialResults) {
        // 合并部分结果的逻辑
        return new Result();
    }
}

五、CountDownLatch使用注意事项

  1. 异常处理:确保在finally块中调用countDown(),避免任务异常导致计数器未减少
scss 复制代码
// ✅ 正确写法
new Thread(() -> {
    try {
        doSomething();
    } catch (Exception e) {
        System.err.println("任务异常:" + e.getMessage());
    } finally {
        latch.countDown(); // 确保在finally中调用
    }
}).start();

// ❌ 错误写法
new Thread(() -> {
    try {
        doSomething();
        latch.countDown(); // 异常时不会执行,导致死锁
    } catch (Exception e) {
        System.err.println("任务异常:" + e.getMessage());
        // 忘记调用countDown()
    }
}).start();
  1. 超时控制:使用带超时的await方法,防止无限等待
csharp 复制代码
boolean finished = latch.await(10, TimeUnit.SECONDS);
if (finished) {
    System.out.println("所有任务完成");
} else {
    System.out.println("等待超时,可能有任务失败");
}
  1. 线程池配合:与ExecutorService一起使用时,注意正确关闭线程池
ini 复制代码
public void useWithThreadPool() throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(5);
    ExecutorService executor = Executors.newFixedThreadPool(3);
    
    for (int i = 0; i < 5; i++) {
        final int taskId = i;
        executor.submit(() -> {
            try {
                System.out.println("执行任务" + taskId);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                latch.countDown();
            }
        });
    }
    
    latch.await();
    executor.shutdown(); // 关闭线程池
    System.out.println("所有任务完成");
}
  1. 计数器设置:初始计数器值应与实际任务数量一致,避免设置过大导致无法唤醒或过小导致提前唤醒
  2. 不可重用性:CountDownLatch是一次性的,若需重复使用考虑CyclicBarrier

六、CountDownLatch与其他同步工具对比

特性 CountDownLatch CyclicBarrier Semaphore
重用性 不可重用 可重用 可重用
计数器方向 递减至0 递增至指定值 许可数可增可减
主要用途 等待事件完成 线程互相等待 控制资源访问
触发动作 支持到达屏障后执行回调
线程关系 主从关系 对等关系 资源竞争关系
典型场景 启动检查、任务等待 分阶段任务 连接池、限流

七、总结

CountDownLatch是Java并发编程中的实用工具,其核心价值在于:

  • 简单易用:API简洁,只需初始化计数器、调用countDown()和await()即可实现线程同步
  • 高效性能:基于AQS实现,底层使用CAS操作,性能优异
  • 灵活应用:适合多种并发协作场景,特别是"主从协作"或"分治合并"模式

适用场景​:

  • 主线程等待多个子任务完成
  • 控制多个线程同时开始执行
  • 分段并行计算后汇总结果
  • 系统启动时的组件初始化

使用口诀​:

  1. 初始化计数 → 2. 任务完成减1 → 3. 归零唤醒主线程

掌握CountDownLatch能够显著提升多线程程序的设计能力与执行效率,是Java开发者必备的并发工具之一。在实际项目中,应根据具体需求合理选择同步工具,并注意异常处理与资源管理,以构建健壮可靠的并发系统。

相关推荐
无名之辈J3 小时前
Spring Boot 对接微信支付
后端
junnhwan3 小时前
【苍穹外卖笔记】Day05--Redis入门与店铺营业状态设置
java·数据库·redis·笔记·后端·苍穹外卖
马尚道4 小时前
【完整版10章】Dubbo 3 深度剖析 - 透过源码认识你
后端
渣哥4 小时前
你以为只是名字不同?Spring 三大注解的真正差别曝光
javascript·后端·面试
Java水解4 小时前
微服务项目->在线oj系统(Java-Spring)----6.0
后端·微服务
艾菜籽4 小时前
Spring Web MVC入门补充1
java·后端·spring·mvc
用户3421674905524 小时前
SVN高级视频教程
后端
洛卡卡了4 小时前
从被动救火到主动预警,接入 Prometheus + Grafana 全流程
后端·面试·架构
无限进步_4 小时前
扫雷游戏的设计与实现:扫雷游戏3.0
c语言·开发语言·c++·后端·算法·游戏·游戏程序