面试必知必会(8):CountDownLatch、CyclicBarrier、Semaphore、Exchanger

Java面试系列文章

面试必知必会(1):线程状态和创建方式

面试必知必会(2):线程池原理

面试必知必会(3):synchronized底层原理

面试必知必会(4):volatile关键字

面试必知必会(5):CAS与原子类

面试必知必会(6):Lock接口及实现类

面试必知必会(7):多线程AQS

面试必知必会(8):CountDownLatch、CyclicBarrier、Semaphore、Exchanger


目录

一、CountDownLatch:等待多个任务全部完成的"计数器"

1. 核心定义

CountDownLatch(倒计时门闩)是一种同步辅助工具,用于让一个或多个线程等待其他多个线程完成各自的任务后,再继续执行自身任务。它的核心是一个递减的计数器,初始化时指定计数器初始值(即需要等待的线程数量)。

注意:CountDownLatch的计数器是一次性的,一旦计数器减至0,后续再调用countDown()方法也不会改变其状态,无法重复使用

2. 核心原理

CountDownLatch基于AQS(AbstractQueuedSynchronizer,抽象队列同步器)实现,其内部维护了一个同步状态state,该state即为计数器的值

  • 初始化时,state = 传入的计数器初始值,此时等待线程(调用await()方法的线程)会被阻塞,加入AQS的等待队列
  • 每个任务线程完成后,调用countDown()方法,会将state的值减1(CAS操作,保证线程安全)
  • 当state的值减至0时,AQS会唤醒等待队列中所有阻塞的线程,这些线程将竞争锁并继续执行

3. 核心方法

  • CountDownLatch(int count):构造函数,设置初始计数值
  • void await():阻塞当前线程,直到计数器为 0
  • boolean await(long timeout, TimeUnit unit):带超时的等待
  • void countDown():将计数器的值减1(CAS操作)
  • getCount():获取当前计数器的剩余值(仅用于查看,可能回去后立即被修改)

4. 典型使用场景

CountDownLatch适合"主等从"的场景,即一个主线程等待多个子线程完成任务后,再执行后续逻辑,常见场景如下:

  • 场景1:主线程等待所有子线程初始化完成,再启动业务逻辑(如服务启动时,等待多个组件加载完成)
  • 场景2:多线程任务执行完成后,主线程统一汇总结果(如多线程统计不同区域的数据,主线程等待所有区域统计完成后,计算总结果)
  • 场景3:模拟并发测试(如让100个线程同时执行某个方法,通过CountDownLatch控制所有线程就绪后,再统一开始执行)

5. 代码实例

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

/**
 * CountDownLatch实例:主线程等待3个子线程完成任务后,汇总结果
 */
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 1. 初始化计数器,指定需要等待的子线程数量(3个)
        CountDownLatch countDownLatch = new CountDownLatch(3);
        
        // 2. 启动3个子线程,每个线程执行任务后调用countDown()
        for (int i = 1; i <= 3; i++) {
            int taskId = i;
            new Thread(() -> {
                try {
                    System.out.println("子线程" + taskId + "开始执行任务");
                    // 模拟任务执行时间(1-3秒,模拟不同任务耗时)
                    TimeUnit.SECONDS.sleep(taskId);
                    System.out.println("子线程" + taskId + "任务执行完成");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 关键:无论任务是否异常,都要调用countDown(),避免主线程永久阻塞
                    countDownLatch.countDown();
                    System.out.println("计数器剩余值:" + countDownLatch.getCount());
                }
            }).start();
        }
        
        System.out.println("主线程等待所有子线程完成任务...");
        // 3. 主线程阻塞,直到计数器为0(也可使用带超时的await,避免永久阻塞)
        // countDownLatch.await(5, TimeUnit.SECONDS); // 超时时间5秒
        countDownLatch.await();
        
        // 4. 所有子线程完成后,主线程执行后续逻辑
        System.out.println("所有子线程任务全部完成,主线程开始汇总结果");
    }
}

输出结果

java 复制代码
主线程等待所有子线程完成任务...
子线程3开始执行任务
子线程2开始执行任务
子线程1开始执行任务
子线程1任务执行完成
计数器剩余值:2
子线程2任务执行完成
计数器剩余值:1
子线程3任务执行完成
计数器剩余值:0
所有子线程任务全部完成,主线程开始汇总结果

6. 注意事项

  • 计数器初始值必须≥0,否则构造方法抛出IllegalArgumentException
  • countDown()方法必须在子线程任务完成后调用(建议放在finally块中),否则若子线程抛出异常未执行countDown(),计数器无法减至0,主线程会永久阻塞
  • await()方法可被中断,若等待线程被中断,会抛出InterruptedException,需妥善处理(如停止后续逻辑、记录日志)
  • CountDownLatch是一次性的,计数器归0后,再调用countDown()无效,若需重复使用,需重新创建实例(后续CyclicBarrier可解决此问题)

二、CyclicBarrier:多线程同步的"栅栏"(可重复使用)

1. 核心定义

CyclicBarrier(循环栅栏)也是一种同步辅助工具,用于让多个线程在某个"栅栏点"处相互等待,直到所有线程都到达该栅栏点后,所有线程才会同时继续执行。

此外,CyclicBarrier 支持在所有线程到达栅栏点后执行一个回调任务(Runnable)。

与CountDownLatch的核心区别:

  • CountDownLatch是"主等从",主线程等待子线程完成;CyclicBarrier是"平等等待",所有线程相互等待,无主从之分
  • CountDownLatch计数器一次性,归0后无法复用;CyclicBarrier栅栏可重复使用(计数器归0后,会自动重置为初始值,支持多轮同步)
  • CountDownLatch的countDown()可在任意线程中调用;CyclicBarrier的"到达栅栏"操作,只能由参与同步的线程自身调用await()触发

2. 核心原理

CyclicBarrier同样基于AQS实现,其内部维护了两个核心变量:

  • parties:参与同步的线程数量(初始化时指定)
  • count:当前等待到达栅栏的线程数量(初始值=parties,每有一个线程调用await(),count减1)

工作流程:

  1. 初始化CyclicBarrier时,指定parties(参与线程数)和一个可选的回调函数Runnable(栅栏触发后,会由最后一个到达栅栏的线程执行该任务)
  2. 每个参与线程执行完自身任务后,调用await()方法,此时线程会阻塞(进入AQS等待队列),同时count减1
  3. 当count减至0时,所有阻塞的线程被唤醒,如何存在回调函数Runnable任务(仅最后一个到达的线程执行)
  4. 所有线程唤醒后,CyclicBarrier的count会自动重置为parties,支持下一轮同步(可重复使用)

3. 核心方法

  • CyclicBarrier(int parties):指定参与线程数
  • CyclicBarrier(int parties, Runnable barrierAction):指定线程数和回调函数
  • int await():等待所有线程到达栅栏点
  • int await(long timeout, TimeUnit unit):带超时的等待

4. 典型使用场景

CyclicBarrier适合"多线程协同完成多轮任务"的场景,所有线程必须同步到达某个节点后,才能进入下一轮,常见场景如下:

  • 场景1:多线程分阶段执行任务(如模拟比赛,所有选手到达起点(栅栏1)后,同时开始比赛;所有选手到达终点(栅栏2)后,同时公布成绩)
  • 场景2:多线程数据并行处理(如处理一个大文件,分成多个分片,每个线程处理一个分片,所有线程处理完成(栅栏点)后,再由一个线程汇总分片结果,然后进入下一轮处理)
  • 场景3:测试多线程同步性能(如让多个线程重复执行"执行任务→等待栅栏"的流程,模拟多轮同步场景)

5. 代码实例(模拟多轮同步)

java 复制代码
public class CyclicBarrierExample {
    public static void main(String[] args) {
        int threadCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("所有线程已就位,开始下一阶段任务!");
        });

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 到达第一阶段");
                    barrier.await(); // 等待其他线程

                    System.out.println(Thread.currentThread().getName() + " 执行第二阶段");
                    Thread.sleep(1000);
                    barrier.await(); // 再次等待,体现"可重用"

                    System.out.println(Thread.currentThread().getName() + " 完成全部任务");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

三、Semaphore:控制并发访问数量的"限流工具"

1. 核心定义

Semaphore(信号量)是一种用于控制同时访问某个共享资源的线程数量的同步工具,本质上是一个"许可证"计数器。它通过发放许可证的方式,限制并发访问的线程数:线程需要先获取许可证才能访问资源,访问完成后释放许可证,其他线程才能获取许可证继续访问。

核心作用:限流、控制并发度,避免因线程过多导致的资源耗尽(如数据库连接池、接口限流、文件读写并发控制)。

2. 核心原理

Semaphore基于AQS实现,其内部维护的state即为"可用许可证数量",初始化时指定许可证总数。

  • 初始化时,state = 许可证总数(permits)
  • 线程调用acquire()方法获取许可证:若state>0,state减1(获取成功,线程继续执行);若state=0,线程阻塞,加入AQS等待队列
  • 线程访问资源完成后,调用release()方法释放许可证:state加1(释放成功),同时唤醒AQS等待队列中阻塞的线程,使其有机会获取许可证

关键细节:Semaphore支持"公平锁"和"非公平锁"模式(构造方法指定):

  • 公平模式(fair=true):等待时间最长的线程优先获取许可证(FIFO),避免线程饥饿,但性能略差
  • 非公平模式(fair=false,默认):线程获取许可证时不按等待顺序,直接竞争,性能更好,但可能导致某些线程长期无法获取许可证(饥饿)

3. 核心方法

  • Semaphore(int permits):创建具有指定许可数的信号量
  • Semaphore(int permits, boolean fair):指定是否公平
  • void acquire():获取一个许可(阻塞)
  • void acquire(int permits):获取多个许可
  • void release():释放一个许可
  • boolean tryAcquire():尝试获取许可,不阻塞
  • int availablePermits():返回当前可用许可数

4. 典型使用场景

Semaphore的核心场景是"限流",控制并发访问的线程数,常见场景如下:

  • 场景1:数据库连接池限流(如数据库最多支持10个并发连接,用Semaphore初始化10个许可证,每个线程获取许可证后获取连接,用完释放许可证,避免连接耗尽)
  • 场景2:接口限流(如某个接口每秒最多允许5个请求并发访问,用Semaphore控制,超过5个请求则等待或拒绝)
  • 场景3:共享资源并发控制(如多个线程读写同一个文件,用Semaphore限制同时读写的线程数,避免文件损坏)

5. 代码实例(模拟接口限流)

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

/**
 * Semaphore实例:模拟接口限流,最多允许3个线程同时访问接口
 */
public class SemaphoreDemo {
    public static void main(String[] args) {
        // 1. 初始化信号量:3个许可证(最多3个线程同时访问),公平模式(避免线程饥饿)
        Semaphore semaphore = new Semaphore(3, true);
        
        // 2. 启动10个线程,模拟10个并发请求访问接口
        for (int i = 1; i <= 10; i++) {
            int requestId = i;
            new Thread(() -> {
                try {
                    System.out.println("请求" + requestId + ":尝试访问接口,获取许可证");
                    // 3. 尝试获取许可证(带超时,避免永久阻塞,超时时间2秒)
                    boolean acquired = semaphore.tryAcquire(2, TimeUnit.SECONDS);
                    if (!acquired) {
                        System.out.println("请求" + requestId + ":获取许可证超时,接口访问失败(限流)");
                        return;
                    }
                    
                    // 4. 获取许可证成功,访问接口(模拟接口耗时)
                    System.out.println("请求" + requestId + ":获取许可证成功,开始访问接口");
                    TimeUnit.SECONDS.sleep(1); // 模拟接口处理耗时1秒
                    System.out.println("请求" + requestId + ":接口访问完成");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("请求" + requestId + ":获取许可证被中断");
                } finally {
                    // 5. 释放许可证(无论访问成功与否,都要释放,避免许可证泄漏)
                    // 注意:只有获取到许可证的线程,才需要释放(此处用acquired判断)
                    if (semaphore.availablePermits() < 3) { // 简单判断是否获取到许可证
                        semaphore.release();
                        System.out.println("请求" + requestId + ":释放许可证,当前可用许可证:" + semaphore.availablePermits());
                    }
                }
            }).start();
            
            // 模拟请求间隔(0.5秒一个请求,模拟高并发)
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

四、Exchanger:线程间双向数据交换的"桥梁"

1. 核心定义

Exchanger(交换器)是一种用于两个线程之间双向交换数据的同步工具,它提供了一个交换点,两个线程可以在该交换点交换各自的数据:线程A到达交换点后,会阻塞等待线程B到达;线程B到达交换点后,会将自己的数据传递给线程A,同时获取线程A的数据,然后两个线程同时继续执行。

核心特点:

  • 仅支持两个线程之间的数据交换,多线程使用时会出现不可预期的结果(如线程A与线程B交换,线程C等待时可能被线程D唤醒,交换错误)
  • 交换的数据是双向的:线程A传递数据给线程B,同时线程B传递数据给线程A,不同于简单的线程间通信(如wait/notify单向传递)

2. 核心原理

Exchanger内部维护了一个"交换槽"(slot),用于存储等待交换的数据,以及一个等待线程的引用,其工作流程如下:

  1. 线程A调用exchange(V x)方法,将自己的数据x放入交换槽,然后阻塞等待线程B
  2. 线程B调用exchange(V y)方法,发现交换槽中已有数据(线程A的数据x),则将自己的数据y放入交换槽,同时获取x,然后唤醒线程A
  3. 线程A被唤醒后,获取交换槽中的y(线程B的数据),两个线程同时继续执行,完成数据交换

补充:若只有一个线程调用exchange()方法,该线程会一直阻塞,直到有另一个线程调用exchange(),或被中断、超时

3. 核心方法

  • Exchanger():构造方法,初始化交换器
  • V exchange(V x):交换数据,阻塞直到配对成功
  • V exchange(V x, long timeout, TimeUnit unit):带超时的交换

4. 典型使用场景

Exchanger适合"两个线程需要相互交换数据"的场景,常见场景如下:

  • 场景1:数据校验(如线程A生成数据,线程B校验数据,线程A将数据传递给线程B,线程B将校验结果传递给线程A)
  • 场景2:数据交换(如线程A处理一半的数据,线程B处理另一半的数据,两者交换处理结果,合并为最终结果)
  • 场景3:生产者-消费者变体(如生产者生产数据,消费者消费数据后返回处理反馈,两者通过Exchanger交换"数据"和"反馈")

5. 代码实例(模拟数据交换与校验)

java 复制代码
import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;

/**
 * Exchanger实例:两个线程交换数据(线程A生成数据,线程B校验数据,交换数据与校验结果)
 */
public class ExchangerDemo {
    public static void main(String[] args) {
        // 1. 初始化交换器,泛型指定交换的数据类型(此处为String)
        Exchanger<String> exchanger = new Exchanger<>();
        
        // 线程1:生成数据,等待与线程2交换(交换数据与校验结果)
        new Thread(() -> {
            String data = null;
            try {
                // 模拟生成数据耗时
                TimeUnit.SECONDS.sleep(1);
                data = "用户信息:id=1001,name=张三";
                System.out.println("线程1:生成数据,准备交换:" + data);
                
                // 2. 调用exchange(),放入数据,阻塞等待线程2交换
                // 可选:带超时的exchange,避免永久阻塞
                // String result = exchanger.exchange(data, 3, TimeUnit.SECONDS);
                String result = exchanger.exchange(data);
                
                // 3. 交换完成,获取线程2的校验结果
                System.out.println("线程1:交换完成,获取线程2的校验结果:" + result);
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("线程1:交换数据失败");
            }
        }, "线程1").start();
        
        // 线程2:接收线程1的数据,进行校验,然后交换校验结果
        new Thread(() -> {
            String checkResult = null;
            try {
                // 模拟准备校验耗时
                TimeUnit.SECONDS.sleep(2);
                System.out.println("线程2:准备接收线程1的数据,进行校验");
                
                // 2. 调用exchange(),阻塞等待线程1的数据,同时传入校验结果
                String data = exchanger.exchange("校验结果:数据格式正确,无异常");
                
                // 3. 交换完成,获取线程1的数据,模拟校验(此处简化校验逻辑)
                System.out.println("线程2:交换完成,获取线程1的数据:" + data);
                // 模拟校验逻辑
                if (data.contains("id=") && data.contains("name=")) {
                    checkResult = "校验结果:数据格式正确,无异常";
                } else {
                    checkResult = "校验结果:数据格式错误,请重新生成";
                }
                System.out.println("线程2:校验完成,校验结果:" + checkResult);
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("线程2:交换数据失败");
            }
        }, "线程2").start();
    }
}

五、总结

本文详细解析了Java并发编程中四个常用工具类的核心细节,它们各自聚焦不同的并发场景,封装了复杂的同步逻辑,帮助开发者简化并发代码编写:

  • CountDownLatch:适合"主等从",一次性等待多任务完成,无需复用
  • CyclicBarrier:适合"多线程平等同步",支持多轮复用,适合分阶段任务
  • Semaphore:适合"限流控制",控制并发访问数量,避免资源耗尽
  • Exchanger:适合"两个线程双向数据交换",原子性交换,简化线程间双向通信
相关推荐
m0_607076608 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
NEXT068 小时前
二叉搜索树(BST)
前端·数据结构·面试
NEXT069 小时前
JavaScript进阶:深度剖析函数柯里化及其在面试中的底层逻辑
前端·javascript·面试
夏鹏今天学习了吗9 小时前
【LeetCode热题100(100/100)】数据流的中位数
算法·leetcode·职场和发展
愚者游世11 小时前
brace-or-equal initializers(花括号或等号初始化器)各版本异同
开发语言·c++·程序人生·面试·visual studio
元亓亓亓12 小时前
LeetCode热题100--42. 接雨水--困难
算法·leetcode·职场和发展
源代码•宸13 小时前
Leetcode—200. 岛屿数量【中等】
经验分享·后端·算法·leetcode·面试·golang·dfs
用户1252055970813 小时前
后端Python+Django面试题
后端·面试
Tracy老板翻译官13 小时前
【团队管理问题篇】别让“凉粉冤案”毁了你的团队
网络·职场和发展·团队开发·创业创新·职场晋升
boooooooom14 小时前
Vue v-for + key 优化封神:吃透就地复用与强制重排,再也不卡帧!
javascript·vue.js·面试