在 Java 并发编程中,除了synchronizedLock用于保证线程安全,还有一类 “并发工具类” 用于协调多个线程的执行顺序 ——CountDownLatch(倒计时门闩)和CyclicBarrier(循环屏障)就是其中的典型代表。它们能轻松实现 “线程等待多线程完成”“多线程等待彼此” 等复杂逻辑,广泛应用于测试、批量任务处理等场景。本文将从原理、区别、实战案例三个维度,详解这两个工具类的使用,帮你解决并发编程中的 “线程协调” 难题。

一、CountDownLatch:让线程等待多线程完成

CountDownLatch基于 “倒计时” 原理,核心功能是让一个或多个线程等待其他线程完成指定操作后,再继续执行。例如,主线程等待 10 个任务线程全部执行完毕后,再统计任务结果。

1.1 核心原理

  • 初始化:创建CountDownLatch时指定一个 “倒计时总数”(count),表示需要等待的线程 / 操作数量;
  • 倒计时:每个线程完成操作后,调用countDown()方法,将count减 1;
  • 等待:需要等待的线程调用await()方法,若count未减至 0,线程会阻塞;当count减至 0 时,所有阻塞的线程被唤醒,继续执行。
原理示意图:

plaintext

主线程                任务线程1      任务线程2      任务线程3
   |                      |             |             |
   | 创建CountDownLatch(3) |             |             |
   |---------------------->|             |             |
   |                      | 执行任务    | 执行任务    | 执行任务
   |                      |------->countDown()(2)     |
   |                      |             |------->countDown()(1)
   | 调用await()阻塞       |             |             |
   |<---------------------|             |             |
   |                      |             |             |------->countDown()(0)
   | 被唤醒,继续执行      |             |             |
   |------->统计结果       |             |             |

1.2 核心方法

方法 作用 注意事项
CountDownLatch(int count) 构造方法,初始化倒计时总数 count必须大于 0,否则抛出IllegalArgumentException
void countDown() 倒计时减 1 count已为 0,调用此方法无效果
void await() 阻塞当前线程,直到count减至 0 可被InterruptedException中断(如其他线程调用interrupt()
boolean await(long timeout, TimeUnit unit) 阻塞指定时间,若超时后count仍未为 0,返回false;否则返回true 避免线程永久阻塞

1.3 实战场景 1:主线程等待多任务线程完成

需求:主线程创建 5 个任务线程,每个线程执行 “模拟数据处理” 操作(耗时 1 秒),主线程等待所有任务线程完成后,打印 “所有任务执行完毕”。

代码实现:

java

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class CountDownLatchDemo1 {
    public static void main(String[] args) throws InterruptedException {
        // 1. 初始化CountDownLatch,倒计时总数=5(5个任务线程)
        CountDownLatch countDownLatch = new CountDownLatch(5);
        
        // 2. 创建5个任务线程
        for (int i = 1; i <= 5; i++) {
            int taskId = i;
            new Thread(() -> {
                try {
                    System.out.println("任务线程" + taskId + "开始执行数据处理...");
                    // 模拟数据处理耗时(1秒)
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("任务线程" + taskId + "执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 3. 任务完成,倒计时减1(必须放在finally中,避免异常导致count未减)
                    countDownLatch.countDown();
                }
            }).start();
        }
        
        System.out.println("主线程等待所有任务线程完成...");
        // 4. 主线程阻塞,直到count减至0
        countDownLatch.await();
        
        // 5. 所有任务线程完成,主线程继续执行
        System.out.println("所有任务执行完毕,主线程开始汇总结果");
    }
}
执行结果:

plaintext

主线程等待所有任务线程完成...
任务线程1开始执行数据处理...
任务线程2开始执行数据处理...
任务线程3开始执行数据处理...
任务线程4开始执行数据处理...
任务线程5开始执行数据处理...
任务线程1执行完毕
任务线程2执行完毕
任务线程3执行完毕
任务线程4执行完毕
任务线程5执行完毕
所有任务执行完毕,主线程开始汇总结果
关键注意点:
  • countDown()必须放在finally块中:若任务线程抛出异常,countDown()仍会执行,避免主线程永久阻塞;
  • 倒计时总数与任务数一致:确保每个任务线程都调用一次countDown(),否则count无法减至 0,主线程会一直阻塞(可使用await(timeout)设置超时时间)。

1.4 实战场景 2:多线程等待初始化完成

需求:系统启动时,需要先完成 “配置加载”“数据库连接初始化”“缓存加载” 3 个初始化操作(每个操作由一个线程执行),待所有初始化完成后,10 个业务线程才能开始处理业务。

代码实现:

java

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class CountDownLatchDemo2 {
    // 初始化倒计时器:需要等待3个初始化操作
    private static final CountDownLatch initLatch = new CountDownLatch(3);

    public static void main(String[] args) {
        // 1. 启动3个初始化线程
        startInitThreads();
        
        // 2. 启动10个业务线程(业务线程会等待初始化完成)
        for (int i = 1; i <= 10; i++) {
            int threadId = i;
            new Thread(() -> {
                try {
                    System.out.println("业务线程" + threadId + "等待系统初始化...");
                    // 等待初始化完成(count减至0)
                    initLatch.await();
                    // 初始化完成,开始处理业务
                    System.out.println("业务线程" + threadId + "开始处理业务");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

    // 启动3个初始化线程:配置加载、数据库初始化、缓存加载
    private static void startInitThreads() {
        // 初始化1:配置加载
        new Thread(() -> {
            try {
                System.out.println("初始化线程-配置加载:开始加载系统配置...");
                TimeUnit.SECONDS.sleep(2); // 模拟耗时
                System.out.println("初始化线程-配置加载:配置加载完成");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                initLatch.countDown();
            }
        }).start();

        // 初始化2:数据库连接初始化
        new Thread(() -> {
            try {
                System.out.println("初始化线程-数据库:开始初始化数据库连接...");
                TimeUnit.SECONDS.sleep(3); // 模拟耗时
                System.out.println("初始化线程-数据库:数据库连接初始化完成");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                initLatch.countDown();
            }
        }).start();

        // 初始化3:缓存加载
        new Thread(() -> {
            try {
                System.out.println("初始化线程-缓存:开始加载缓存数据...");
                TimeUnit.SECONDS.sleep(1); // 模拟耗时
                System.out.println("初始化线程-缓存:缓存加载完成");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                initLatch.countDown();
            }
        }).start();
    }
}
执行结果(部分):

plaintext

初始化线程-配置加载:开始加载系统配置...
初始化线程-数据库:开始初始化数据库连接...
初始化线程-缓存:开始加载缓存数据...
业务线程1等待系统初始化...
业务线程2等待系统初始化...
...(省略其他业务线程)
初始化线程-缓存:缓存加载完成
初始化线程-配置加载:配置加载完成
初始化线程-数据库:数据库连接初始化完成
业务线程1开始处理业务
业务线程2开始处理业务
...(所有业务线程开始执行)
核心价值:
  • 实现 “一次性初始化,多线程复用”:所有业务线程无需各自等待初始化,只需等待一次initLatchcount减至 0;
  • 解耦初始化与业务逻辑:初始化线程和业务线程通过CountDownLatch通信,无需硬编码依赖。

二、CyclicBarrier:让多线程互相等待

CyclicBarrier基于 “屏障” 原理,核心功能是让多个线程互相等待,直到所有线程都到达指定 “屏障点” 后,再一起继续执行。例如,3 个线程共同执行一个任务,需先各自完成 “数据采集”,待所有线程都采集完数据后,再一起执行 “数据汇总”。

2.1 核心原理

  • 初始化:创建CyclicBarrier时指定 “参与线程数”(parties),表示需要等待的线程数量;
  • 到达屏障:每个线程执行到屏障点时,调用await()方法,线程会阻塞,直到所有parties个线程都调用await()
  • 屏障突破:当最后一个线程调用await()后,所有阻塞的线程被唤醒,一起继续执行;若设置了 “屏障动作”(Runnable),会先执行屏障动作,再唤醒线程。
原理示意图:

plaintext

线程A                线程B                线程C
   |                   |                   |
   | 执行任务阶段1      | 执行任务阶段1      | 执行任务阶段1
   |------->到达屏障点  |------->到达屏障点  |
   | 调用await()阻塞    | 调用await()阻塞    |
   |<------------------|------------------|
   |                   |                   |------->到达屏障点
   |                   |                   | 调用await()
   | 屏障动作执行(可选)|                   |
   |------->所有线程被唤醒                   |
   | 执行任务阶段2      | 执行任务阶段2      | 执行任务阶段2

2.2 核心方法

方法 作用 注意事项
CyclicBarrier(int parties) 构造方法,初始化参与线程数 parties必须大于 0
CyclicBarrier(int parties, Runnable barrierAction) 构造方法,指定参与线程数和屏障动作 屏障动作由最后一个到达屏障的线程执行
int await() 到达屏障,阻塞线程直到所有线程到达 可被InterruptedException中断;若其他线程被中断,当前线程会抛出BrokenBarrierException
int await(long timeout, TimeUnit unit) 阻塞指定时间,超时后抛出TimeoutException 避免线程永久阻塞
void reset() 重置屏障为初始状态 若有线程已阻塞在屏障点,会抛出BrokenBarrierException

2.3 核心特性:可循环使用

CountDownLatch(倒计时一旦为 0,无法重置)不同,CyclicBarrier支持循环使用—— 当所有线程突破一次屏障后,屏障会自动重置,可再次用于下一轮线程等待。例如,3 个线程可重复执行 “阶段 1→屏障→阶段 2” 的流程。

2.4 实战场景 1:多线程分阶段执行任务

需求:4 个线程共同处理一个 “数据统计” 任务,分两个阶段执行:

  1. 阶段 1:每个线程采集各自负责的数据源数据(耗时 1-3 秒);
  2. 阶段 2:所有线程采集完数据后,一起汇总数据(需先执行 “屏障动作”:打印 “所有线程已完成数据采集”)。
代码实现:

java

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;

public class CyclicBarrierDemo1 {
    public static void main(String[] args) {
        // 1. 初始化CyclicBarrier:参与线程数=4,屏障动作(最后一个线程到达后执行)
        CyclicBarrier cyclicBarrier = new CyclicBarrier(4, () -> {
            System.out.println("=== 屏障动作执行:所有线程已完成数据采集,准备进入汇总阶段 ===");
        });

        // 2. 创建4个线程,每个线程负责一个数据源
        for (int i = 1; i <= 4; i++) {
            int dataSourceId = i;
            new Thread(() -> {
                try {
                    // 阶段1:采集数据
                    System.out.println("线程" + Thread.currentThread().getId() + "(数据源" + dataSourceId + ")开始采集数据...");
                    // 模拟采集耗时(1-3秒随机)
                    TimeUnit.SECONDS.sleep((long) (Math.random() * 2 + 1));
                    System.out.println("线程" + Thread.currentThread().getId() + "(数据源" + dataSourceId + ")数据采集完成,到达屏障点");

                    // 到达屏障,等待其他线程
                    cyclicBarrier.await();

                    // 阶段2:汇总数据(所有线程被唤醒后执行)
                    System.out.println("线程" + Thread.currentThread().getId() + "(数据源" + dataSourceId + ")开始汇总数据");
                    TimeUnit.SECONDS.sleep(1); // 模拟汇总耗时
                    System.out.println("线程" + Thread.currentThread().getId() + "(数据源" + dataSourceId + ")数据汇总完成");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
执行结果(部分):

plaintext

线程11(数据源1)开始采集数据...
线程12(数据源2)开始采集数据...
线程13(数据源3)开始采集数据...
线程14(数据源4)开始采集数据...
线程12(数据源2)数据采集完成,到达屏障点
线程11(数据源1)数据采集完成,到达屏障点
线程14(数据源4)数据采集完成,到达屏障点
线程13(数据源3)数据采集完成,到达屏障点
=== 屏障动作执行:所有线程已完成数据采集,准备进入汇总阶段 ===
线程13(数据源3)开始汇总数据
线程14(数据源4)开始汇总数据
线程11(数据源1)开始汇总数据
线程12(数据源2)开始汇总数据
线程13(数据源3)数据汇总完成
线程14(数据源4)数据汇总完成
线程11(数据源1)数据汇总完成
线程12(数据源2)数据汇总完成
关键观察点:
  • 屏障动作由最后一个到达屏障的线程执行(如线程 13);
  • 所有线程在屏障动作执行后,才一起进入阶段 2,实现 “分阶段同步”。

2.5 实战场景 2:循环执行多轮任务

需求:2 个线程模拟 “运动员训练”,需完成 3 轮训练,每轮训练流程为:

  1. 各自完成 “热身”(耗时 1 秒);
  2. 所有线程热身完成后,一起执行 “高强度训练”(屏障动作);
  3. 重复 3 轮。
代码实现:

java

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;

public class CyclicBarrierDemo2 {
    public static void main(String[] args) {
        // 初始化CyclicBarrier:参与线程数=2,屏障动作(每轮热身完成后执行)
        CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
            System.out.println("=== 所有运动员热身完成,开始高强度训练 ===");
            try {
                TimeUnit.SECONDS.sleep(2); // 模拟高强度训练耗时
                System.out.println("=== 本轮高强度训练结束,休息1秒准备下一轮 ===");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 定义训练线程(2个运动员)
        Runnable athleteTask = () -> {
            try {
                // 完成3轮训练
                for (int round = 1; round <= 3; round++) {
                    System.out.println("运动员" + Thread.currentThread().getId() + ":第" + round + "轮热身开始");
                    TimeUnit.SECONDS.sleep(1); // 模拟热身耗时
                    System.out.println("运动员" + Thread.currentThread().getId() + ":第" + round + "轮热身完成,等待队友");

                    // 到达屏障,等待队友(每轮都会等待)
                    cyclicBarrier.await();
                }
                System.out.println("运动员" + Thread.currentThread().getId() + ":所有训练轮次完成!");
            } catch (Exception e) {
                e.printStackTrace();
            }
        };

        // 启动2个运动员线程
        new Thread(athleteTask).start();
        new Thread(athleteTask).start();
    }
}
执行结果(部分):

plaintext

运动员11:第1轮热身开始
运动员12:第1轮热身开始
运动员11:第1轮热身完成,等待队友
运动员12:第1轮热身完成,等待队友
=== 所有运动员热身完成,开始高强度训练 ===
=== 本轮高强度训练结束,休息1秒准备下一轮 ===
运动员12:第2轮热身开始
运动员11:第2轮热身开始
运动员12:第2轮热身完成,等待队友
运动员11:第2轮热身完成,等待队友
=== 所有运动员热身完成,开始高强度训练 ===
=== 本轮高强度训练结束,休息1秒准备下一轮 ===
...(省略第3轮)
运动员11:所有训练轮次完成!
运动员12:所有训练轮次完成!
核心价值:
  • 循环复用屏障:无需创建多个CyclicBarrier,一个实例即可支持多轮线程同步;
  • 简化多轮任务逻辑:每轮任务的同步逻辑统一,无需手动管理轮次状态。

三、CountDownLatch 与 CyclicBarrier 的核心区别

很多开发者会混淆CountDownLatchCyclicBarrier,但两者的设计目标和使用场景差异显著,核心区别如下:

对比维度 CountDownLatch CyclicBarrier
核心功能 一个 / 多个线程等待其他线程完成 多个线程互相等待,一起突破屏障
复用性 不可复用(count 减至 0 后,无法重置) 可循环复用(突破屏障后自动重置)
参与角色 分 “等待方” 和 “被等待方”(角色不同) 所有线程角色相同(均为 “等待方”)
屏障动作 无(需额外线程实现类似逻辑) 支持屏障动作(最后一个线程到达后执行)
异常处理 被等待方抛出异常,需确保countDown()执行(放在 finally) 某线程被中断,会导致其他线程抛出BrokenBarrierException
典型场景 主线程等待多任务完成、多线程等待初始化 多线程分阶段执行、循环任务同步
记忆口诀:
  • 「Latch 倒计时,一次用完就失效;Barrier 循环等,线程平等同前行」

四、常见问题与最佳实践

4.1 常见问题

  1. CountDownLatch 的 count 未减至 0 导致线程永久阻塞:解决方法:使用await(long timeout, TimeUnit unit)设置超时时间,或在finally块中调用countDown()
  2. CyclicBarrier 的线程被中断导致屏障损坏:解决方法:捕获BrokenBarrierException,调用cyclicBarrier.reset()重置屏障,重新执行任务;
  3. 线程数量与 parties/count 不匹配:例如,CyclicBarrier(3)但只有 2 个线程调用await(),会导致线程永久阻塞。解决方法:确保参与线程数与初始化时的数量一致。

4.2 最佳实践

  1. 明确使用场景
    • 若需 “单向等待”(如主线程等任务线程),用CountDownLatch
    • 若需 “双向等待”(如多线程分阶段执行),用CyclicBarrier
  2. 设置超时时间:无论是CountDownLatch.await()还是CyclicBarrier.await(),都建议设置超时时间,避免线程因异常永久阻塞;
  3. 优先使用线程池:实际开发中,避免手动创建线程,应使用ThreadPoolExecutor管理线程,结合并发工具类使用,提升资源利用率;
  4. 日志记录关键节点:在countDown()await()前后打印日志,方便问题排查(如定位哪个线程未调用countDown())。

五、总结

CountDownLatchCyclicBarrier是 Java 并发编程中 “线程协调” 的利器,它们分别解决了 “单向等待” 和 “双向等待” 的问题,避免了复杂的wait()/notify()逻辑(易出错且效率低)。

  • CountDownLatch:适合 “一个或多个线程等待其他线程完成” 的场景,如任务汇总、系统初始化;
  • CyclicBarrier:适合 “多个线程分阶段同步执行” 的场景,如多轮训练、分阶段数据处理。

在实际开发中,需根据线程角色和任务流程选择合适的工具类,并注意异常处理和资源释放,才能写出高效、安全的并发代码。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐