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开发者必备的并发工具之一。在实际项目中,应根据具体需求合理选择同步工具,并注意异常处理与资源管理,以构建健壮可靠的并发系统。

相关推荐
鬼火儿5 小时前
SpringBoot】Spring Boot 项目的打包配置
java·后端
cr7xin5 小时前
缓存三大问题及解决方案
redis·后端·缓存
间彧6 小时前
Kubernetes的Pod与Docker Compose中的服务在概念上有何异同?
后端
间彧7 小时前
从开发到生产,如何将Docker Compose项目平滑迁移到Kubernetes?
后端
间彧7 小时前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧7 小时前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧7 小时前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧7 小时前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端
间彧7 小时前
如何在实际项目中集成Arthas Tunnel Server实现Kubernetes集群的远程诊断?
后端
brzhang7 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构