Java 并发工具类详解:4 大核心工具 + 实战场景,告别 synchronized

在 Java 并发编程中,synchronized虽然能解决线程同步问题,但功能单一、灵活性差,面对复杂场景(如多线程等待、流量控制、数据交换)时力不从心。JDK 提供的CountDownLatchCyclicBarrierSemaphoreExchanger四大并发工具类,基于 AQS(队列式同步器)实现,能更优雅地处理各类并发场景。本文用 "生活场景 + 代码示例" 的方式,带你吃透这 4 个工具类,面试和工作都能用得上!

一、为什么需要并发工具类?------ 解决synchronized的痛点

synchronized作为 Java 基础的同步锁,存在 3 个明显局限:

  • 只能实现 "互斥"(同一时间一个线程访问资源),无法实现 "协作"(多个线程按顺序执行);
  • 没有超时机制,线程可能永久阻塞;
  • 功能单一,无法满足流量控制、数据交换等复杂需求。

而并发工具类的核心价值的是 **"线程协作与精细化控制"**,让多线程不再是 "各自为战",而是能按预期流程协同工作,同时提供超时、回调等高级特性,让并发编程更安全、更灵活。

二、4 大核心并发工具类:场景 + 代码 + 原理

1. CountDownLatch:倒计时锁 ------"等待其他线程完成,再行动"

核心定位

允许 1 个或多个线程等待,直到其他N个线程完成任务后,再继续执行。本质是 "倒计数器",计数器归 0 时,等待线程被唤醒。

生活场景
  • 考试结束:老师(等待线程)要等所有学生(工作线程)交卷后,才开始批改试卷;
  • 田径赛跑:10 名运动员(工作线程)等待裁判(控制线程)发令后,同时起跑。
核心方法
  • CountDownLatch(int count):构造方法,初始化计数器(需等待的线程数);
  • countDown():工作线程调用,计数器减 1(任务完成);
  • await():等待线程调用,阻塞直到计数器为 0;
  • await(long timeout, TimeUnit unit):带超时的等待,避免永久阻塞。
实战代码
场景 1:主线程等待 10 个工作线程完成后汇总结果
java 复制代码
import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo1 {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 10; // 10个工作线程
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 模拟工作任务(如数据查询、文件处理)
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + ":任务执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown(); // 计数器减1(必须放在finally,确保任务完成后执行)
                }
            }, "工作线程" + i).start();
        }

        System.out.println("主线程:等待所有工作线程完成...");
        latch.await(); // 阻塞等待计数器为0
        System.out.println("主线程:所有任务执行完毕,开始汇总结果");
    }
}

运行结果

java 复制代码
主线程:等待所有工作线程完成...
工作线程0:任务执行完毕
工作线程1:任务执行完毕
...(中间省略8个线程)
工作线程9:任务执行完毕
主线程:所有任务执行完毕,开始汇总结果
场景 2:10 个线程等待指令,同时执行
java 复制代码
import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo2 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1); // 计数器初始为1(等待1个信号)
        int threadCount = 10;

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + ":准备就绪,等待指令");
                    latch.await(); // 阻塞等待计数器为0
                    System.out.println(Thread.currentThread().getName() + ":收到指令,开始执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "运动员" + i).start();
        }

        // 模拟裁判准备时间
        Thread.sleep(2000);
        System.out.println("主线程(裁判):发令!");
        latch.countDown(); // 计数器减1(变为0,唤醒所有等待线程)
    }
}

运行结果

java 复制代码
运动员0:准备就绪,等待指令
运动员1:准备就绪,等待指令
...(中间省略8个线程)
运动员9:准备就绪,等待指令
主线程(裁判):发令!
运动员0:收到指令,开始执行
运动员1:收到指令,开始执行
...(所有线程同时执行)
关键注意
  • 计数器只能使用一次,归 0 后再调用countDown()无效;
  • countDown()必须放在finally块中,确保工作线程无论是否异常,都会 decrement 计数器,避免等待线程永久阻塞。

2. CyclicBarrier:循环屏障 ------"大家到齐了,再一起出发"

核心定位

N个线程到达 "屏障" 后阻塞,直到所有线程都到达屏障,屏障才会解除,所有线程同时继续执行。与CountDownLatch的核心区别是:计数器可循环使用

生活场景
  • 团队旅游:导游(屏障)要等所有游客(线程)到齐后,才出发去下一个景点;
  • 多阶段任务:多个线程先完成第一阶段任务,到屏障处集合,再同时开始第二阶段任务。
核心方法
  • CyclicBarrier(int parties):构造方法,指定参与的线程数(屏障触发条件);
  • CyclicBarrier(int parties, Runnable barrierAction):带回调的构造方法,所有线程到达屏障后,先执行barrierAction(如日志记录、资源初始化);
  • await():线程到达屏障后调用,阻塞直到所有线程都到达;
  • await(long timeout, TimeUnit unit):带超时的等待。
实战代码
java 复制代码
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        int threadCount = 5; // 5个线程参与
        // 屏障触发时,执行回调(所有线程到齐后执行)
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("\n===== 所有线程已到齐,屏障解除!=====\n");
        });

        for (int i = 0; i < threadCount; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    // 模拟线程到达屏障前的任务(如数据预处理)
                    Thread.sleep((long) (Math.random() * 2000));
                    System.out.println(Thread.currentThread().getName() + ":已到达屏障(完成第" + finalI + "阶段任务)");
                    
                    barrier.await(); // 阻塞等待其他线程
                    
                    // 屏障解除后,执行后续任务
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + ":继续执行第" + (finalI + 1) + "阶段任务");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "工作线程" + i).start();
        }
    }
}

运行结果

java 复制代码
工作线程2:已到达屏障(完成第2阶段任务)
工作线程0:已到达屏障(完成第0阶段任务)
工作线程1:已到达屏障(完成第1阶段任务)
工作线程3:已到达屏障(完成第3阶段任务)
工作线程4:已到达屏障(完成第4阶段任务)

===== 所有线程已到齐,屏障解除!=====

工作线程4:继续执行第5阶段任务
工作线程0:继续执行第1阶段任务
工作线程2:继续执行第3阶段任务
工作线程1:继续执行第2阶段任务
工作线程3:继续执行第4阶段任务
关键注意
  • 计数器可循环使用:屏障解除后,计数器自动重置,可再次用于下一轮线程协作;
  • 若某个线程中断或超时,屏障会被破坏,其他线程会抛出BrokenBarrierException,需捕获处理。

3. Semaphore:信号量 ------"流量控制,最多 N 个线程同时访问"

核心定位

控制同一时间访问某个资源的线程数,本质是 "许可计数器",线程需先获取许可才能访问资源,用完后释放许可。

生活场景
  • 停车场:最多容纳 100 辆车(许可数 100),车辆(线程)进入前需获取车位(许可),离开时释放车位;
  • 接口限流:同一时间最多允许 10 个请求(线程)访问支付接口,避免服务过载。
核心方法
  • Semaphore(int permits):构造方法,指定最大许可数(同时访问的线程数);
  • acquire():获取 1 个许可,若无许可则阻塞;
  • tryAcquire(long timeout, TimeUnit unit):尝试在指定时间内获取许可,成功返回true,失败返回false
  • release():释放 1 个许可,唤醒等待的线程;
  • availablePermits():获取当前可用的许可数。
实战代码
场景:接口限流(同一时间最多 3 个线程访问)
java 复制代码
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    public static void main(String[] args) {
        int maxPermits = 3; // 最多3个线程同时访问
        Semaphore semaphore = new Semaphore(maxPermits);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        // 模拟10个请求线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                // 尝试3秒内获取许可,避免永久阻塞
                try {
                    if (semaphore.tryAcquire(3, java.util.concurrent.TimeUnit.SECONDS)) {
                        // 模拟接口处理时间(1-2秒)
                        Thread.sleep((long) (Math.random() * 1000 + 1000));
                        System.out.println(sdf.format(new Date()) + " | " + Thread.currentThread().getName() + ":获取许可,接口访问成功");
                    } else {
                        System.out.println(sdf.format(new Date()) + " | " + Thread.currentThread().getName() + ":获取许可超时,接口访问失败");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放许可(必须放在finally,避免许可泄露)
                    if (semaphore.hasQueuedThreads()) {
                        semaphore.release();
                    }
                }
            }, "请求线程" + i).start();
        }
    }
}

运行结果

java 复制代码
2024-05-20 15:30:01 | 请求线程0:获取许可,接口访问成功
2024-05-20 15:30:01 | 请求线程1:获取许可,接口访问成功
2024-05-20 15:30:01 | 请求线程2:获取许可,接口访问成功
2024-05-20 15:30:02 | 请求线程3:获取许可,接口访问成功
2024-05-20 15:30:02 | 请求线程4:获取许可,接口访问成功
...(后续线程依次获取释放的许可)
关键注意
  • 许可必须手动释放,且要放在finally块中,避免因线程异常导致许可泄露(可用许可越来越少);
  • 当许可数为 1 时,Semaphore等价于互斥锁(synchronized),但更灵活(支持超时、非阻塞获取)。

4. Exchanger:数据交换器 ------"两个线程的双向数据交换"

核心定位

用于两个线程之间的双向数据交换,线程到达交换点后,会阻塞等待另一个线程,直到两个线程都到达,才交换彼此的数据。

生活场景
  • 快递交换:两个快递员(线程)在指定地点碰面,交换各自的包裹(数据);
  • 数据校验:线程 A 读取文件 A 的数据,线程 B 读取文件 B 的数据,交换后互相校验。
核心方法
  • Exchanger<V>:泛型类,指定交换的数据类型;
  • exchange(V x):线程调用,将数据x传递给对方,同时接收对方的数据,阻塞直到对方到达交换点;
  • exchange(V x, long timeout, TimeUnit unit):带超时的交换,超时抛出异常。
实战代码
java 复制代码
import java.util.concurrent.Exchanger;

public class ExchangerDemo {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>(); // 交换String类型数据

        // 线程1:发送数据"A",接收线程2的数据
        new Thread(() -> {
            try {
                String data = "A";
                System.out.println(Thread.currentThread().getName() + ":准备交换的数据:" + data);
                // 阻塞等待线程2,交换数据
                String receivedData = exchanger.exchange(data);
                System.out.println(Thread.currentThread().getName() + ":交换后收到的数据:" + receivedData);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "交换线程1").start();

        // 线程2:发送数据"B",接收线程1的数据
        new Thread(() -> {
            try {
                String data = "B";
                System.out.println(Thread.currentThread().getName() + ":准备交换的数据:" + data);
                // 模拟延迟到达交换点
                Thread.sleep(1000);
                String receivedData = exchanger.exchange(data);
                System.out.println(Thread.currentThread().getName() + ":交换后收到的数据:" + receivedData);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "交换线程2").start();
    }
}

运行结果

java 复制代码
交换线程1:准备交换的数据:A
交换线程2:准备交换的数据:B
交换线程1:交换后收到的数据:B
交换线程2:交换后收到的数据:A
关键注意
  • 仅支持两个线程交换,多个线程调用exchange()会导致数据交换混乱;
  • 若只有一个线程到达交换点,会一直阻塞(或超时),需确保两个线程都能正常到达。

三、高频面试:4 大工具类核心区别

工具类 核心作用 计数器特性 适用场景 核心区别
CountDownLatch 1 个 / 多个线程等待 N 个线程完成 一次性使用 任务汇总、指令下发 等待线程不参与任务,仅等待;计数器不可循环
CyclicBarrier N 个线程互相等待,到齐后同时执行 可循环使用 多阶段任务、团队协作 所有线程都参与任务,到达屏障后继续;计数器可重置
Semaphore 控制同时访问资源的线程数 可重复获取释放 接口限流、资源池控制 不涉及线程等待,仅流量控制
Exchanger 两个线程双向数据交换 无计数器 数据交换、双向校验 仅支持两个线程,专注数据交换

四、实战避坑指南

  1. 避免永久阻塞 :优先使用带超时的方法(如await(10, SECONDS)tryAcquire(3, SECONDS)),防止线程因异常或死锁永久阻塞;
  2. 释放资源 / 许可countDown()release()必须放在finally块中,确保即使线程异常,也能正常释放,避免资源泄露;
  3. 线程中断处理 :并发工具类的阻塞方法会响应interrupt(),需捕获InterruptedException,根据业务决定是继续执行还是退出;
  4. ** CyclicBarrier 异常处理 **:若某个线程中断,会导致屏障破裂,其他线程抛出BrokenBarrierException,可通过isBroken()方法判断屏障状态,必要时重置。

五、总结

Java 并发工具类是synchronized的进阶替代方案,专注于 "线程协作" 和 "精细化控制":

  • 需 "等待其他线程完成" 用CountDownLatch
  • 需 "线程互相等待、循环协作" 用CyclicBarrier
  • 需 "流量控制、限制并发数" 用Semaphore
  • 需 "两个线程数据交换" 用Exchanger

这些工具类底层都基于 AQS 实现,无需关注复杂的锁机制,只需根据场景选择合适的工具,就能让并发编程更简洁、更安全。建议结合实际场景多敲代码练习,理解它们的适用边界,面试时就能从容应对,工作中也能快速解决并发问题。

相关推荐
有位神秘人2 小时前
Android中Notification的使用详解
android·java·javascript
tb_first3 小时前
LangChain4j简单入门
java·spring boot·langchain4j
独自破碎E3 小时前
【BISHI9】田忌赛马
android·java·开发语言
范纹杉想快点毕业3 小时前
实战级ZYNQ中断状态机FIFO设计
java·开发语言·驱动开发·设计模式·架构·mfc
smileNicky3 小时前
布隆过滤器怎么提高误差率
java
それども3 小时前
分库分表的事务问题 - 怎么实现事务
java·数据库·mysql
Java面试题总结4 小时前
基于 Java 的 PDF 文本水印实现方案(iText7 示例)
java·python·pdf
马猴烧酒.4 小时前
【面试八股|Java集合】Java集合常考面试题详解
java·开发语言·python·面试·八股
测试工程师成长之路4 小时前
Serenity BDD 框架:Java + Selenium 全面指南(2026 最新)
java·开发语言·selenium