摘要: 本文深入解析 Java 并发编程的核心工具集 JUC(java.util.concurrent),涵盖线程池、阻塞队列、同步辅助类等关键组件。通过实际代码示例,详细讲解如何利用 ExecutorService 管理线程生命周期、使用 BlockingQueue 实现线程安全队列、借助 CountDownLatch、CyclicBarrier 和 Semaphore 协调线程执行,以及 Fork/Join 框架和 CompletableFuture 的高级应用。掌握这些工具能显著提升并发编程效率,让代码从"手工打造"升级为"标准化生产"。
关键词: Java并发编程, JUC工具集, 线程池, 阻塞队列, CompletableFuture
在过去的几篇文章里,我们从线程的创建开始,学到了 synchronized 和 Lock 来保护共享数据,又学了 wait()/notify() 和 Condition 来实现线程间的协作。
你现在应该对"多线程怎么工作"有了比较深刻的理解。
但是,你也可能感觉到了:这些工具用起来有点"原始"。
- 每次需要执行一个异步任务,都要手动
new Thread(...).start(),创建和销毁线程的开销不小。 - 用
synchronized保护共享数据,用wait()/notify()协调线程,代码写起来繁琐,稍不注意就会引入死锁或性能问题。 - 如果我们需要限制同时访问某个资源的线程数量(比如数据库连接池最多10个连接),用
synchronized来实现会非常复杂。
好在,Java 的设计者早就想到了这些。从 Java 5 开始,java.util.concurrent(简称 JUC)包被引入,它提供了一整套现成的、经过充分测试的高性能并发工具。
今天,我们就来认识这些工具中最常用、最核心的几个:
- 线程池(
ExecutorService) ------ 管理线程的生命周期,复用线程。 - 阻塞队列(
BlockingQueue) ------ 线程安全的队列,自带等待通知。 - 同步辅助类(
CountDownLatch、CyclicBarrier、Semaphore) ------ 让线程之间可以"协同"完成某个任务。 Fork/Join框架 ------ 专门用于分治任务的并行计算框架。
这些工具大大简化了并发编程的复杂度。学会它们,你的并发代码会从"手工打造"变成"使用标准化零件"的现代化生产。
1. 线程池 ------ 管理线程的"管家"
在之前的例子中,我们每次需要执行任务时,都会 new Thread(task).start()。这在任务数量少的时候没问题,但如果你的应用需要创建成千上万个线程(比如一个 Web 服务器要处理数万个并发请求),频繁创建和销毁线程会带来巨大的开销------每次创建线程都要分配栈内存、注册到操作系统,销毁时也要释放资源。
线程池 的核心思想是:预先创建好一批线程,让它们"随时待命"。当有任务需要执行时,从池子里取出一个空闲线程来执行任务;任务执行完后,线程不销毁,而是回到池子里等待下一个任务。这样就复用了线程,大大减少了创建和销毁的开销。
1.1 创建线程池 ------ Executors 工厂类
java.util.concurrent.Executors 提供了几种常用的线程池工厂方法:
java
import java.util.concurrent.*;
public class ExecutorsDemo {
public static void main(String[] args) {
// 1. 固定大小的线程池(核心线程数 = 最大线程数)
ExecutorService fixedPool = Executors.newFixedThreadPool(5);
// 2. 单线程的线程池(只有一个线程,保证任务按顺序执行)
ExecutorService singlePool = Executors.newSingleThreadExecutor();
// 3. 可缓存的线程池(核心线程数为0,最大线程数无限,空闲线程60秒后回收)
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 4. 定时任务线程池(支持延时执行和周期性执行)
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
// 5. 工作窃取线程池(Java 8+,基于 Fork/Join 框架)
ExecutorService workStealingPool = Executors.newWorkStealingPool();
}
}
建议 :在阿里巴巴 Java 开发手册中,不推荐直接使用
Executors工厂方法创建线程池,因为某些方法(如newFixedThreadPool和newCachedThreadPool)使用的是无界队列或无限创建线程,可能在某些极端情况下导致 OOM。更安全的方式是直接用ThreadPoolExecutor的构造方法,显式指定所有参数。我们在本篇文章中为了演示简洁,先用Executors,但在生产环境中建议使用ThreadPoolExecutor显式构造。
1.2 提交任务 ------ execute() 和 submit()
有了线程池,提交任务非常简单:
java
ExecutorService pool = Executors.newFixedThreadPool(3);
// 提交无返回值的任务(Runnable)
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务");
});
// 提交有返回值的任务(Callable)
Future<Integer> future = pool.submit(() -> {
Thread.sleep(1000);
return 42;
});
try {
Integer result = future.get(); // 阻塞等待结果
System.out.println("结果:" + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 关闭线程池(不再接受新任务,等待已有任务完成)
pool.shutdown(); // 优雅关闭
// pool.shutdownNow(); // 立即尝试关闭
execute() vs submit():
execute(Runnable):提交任务,不关心返回值,无返回结果。submit(Callable)或submit(Runnable):提交任务,返回Future<T>,可以获取执行结果或取消任务。
1.3 Future ------ 异步任务的"结果凭证"
Future 代表一个异步计算的结果 。你可以在提交任务后继续做其他事情,然后在需要结果的时候调用 get() 来等待结果。
java
Future<String> future = pool.submit(() -> {
// 模拟耗时操作
Thread.sleep(2000);
return "完成!";
});
// 做其他事情...
System.out.println("主线程继续执行...");
// 稍后获取结果(会阻塞)
String result = future.get(3, TimeUnit.SECONDS); // 最多等待3秒,超时抛出 TimeoutException
System.out.println(result);
Future 的常用方法:
| 方法 | 说明 |
|---|---|
get() |
阻塞等待结果,直到任务完成 |
get(long timeout, TimeUnit unit) |
带超时的等待 |
isDone() |
检查任务是否已完成 |
isCancelled() |
检查任务是否已被取消 |
cancel(boolean mayInterruptIfRunning) |
尝试取消任务 |
注意:
Future.get()会阻塞当前线程。如果需要异步回调,可以配合CompletableFuture(我们会在后面简要提到)。
1.4 ThreadPoolExecutor ------ 更精细的配置
如果你需要更精细地控制线程池的行为,可以用 ThreadPoolExecutor 的构造方法:
java
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, // 核心线程数(始终保留)
5, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10), // 任务队列(容量10)
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用者线程执行
);
// 参数解读:
// - 当任务数 ≤ 2 时,只使用核心线程。
// - 当任务数 > 2 但 ≤ 12(2个核心 + 10个队列)时,新任务进入队列等待。
// - 当任务数 > 12 时,创建新线程(最多到5个)来执行任务。
// - 当任务数 > 15(5个最大线程 + 10个队列)时,触发拒绝策略。
拒绝策略(RejectedExecutionHandler):
AbortPolicy(默认):抛出RejectedExecutionException。CallerRunsPolicy:由提交任务的线程自己执行该任务(有"反压"作用)。DiscardPolicy:丢弃任务,不抛异常。DiscardOldestPolicy:丢弃队列中最旧的任务,再尝试提交新任务。
2. 阻塞队列 ------ BlockingQueue
我们在上一篇文章中已经简单提到了 BlockingQueue。它是 JUC 包中提供的一种线程安全的队列 ,在队列为空时,take() 操作会阻塞;在队列已满时,put() 操作会阻塞。
BlockingQueue 的主要实现:
| 实现类 | 特点 |
|---|---|
ArrayBlockingQueue |
有界队列,基于数组,FIFO |
LinkedBlockingQueue |
可选有界(默认无界),基于链表,FIFO |
PriorityBlockingQueue |
无界优先队列,元素按自然顺序或 Comparator 排序 |
SynchronousQueue |
容量为0,每个 put() 必须等待一个 take() |
DelayQueue |
无界队列,元素只有延迟期满才能被取出 |
LinkedTransferQueue |
无界,支持 transfer()(直接交给消费者) |
java
// 生产者-消费者模式的一个典型用法
BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
// 生产者
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
queue.put("消息-" + i); // 队列满时阻塞
System.out.println("生产:" + i);
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
String msg = queue.take(); // 队列空时阻塞
System.out.println("消费:" + msg);
Thread.sleep(300);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
BlockingQueue 的方法分为四类(取决于是否阻塞、是否抛出异常):
| 操作 | 抛异常 | 返回特殊值 | 阻塞 | 超时阻塞 |
|---|---|---|---|---|
| 插入 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
| 移除 | remove() |
poll() |
take() |
poll(time, unit) |
| 检查 | element() |
peek() |
--- | --- |
在实际开发中,put()/take() 是最常用的组合。
3. 同步辅助类 ------ 让线程"协同"
JUC 提供了几个同步辅助类,它们本身不存储数据,而是用来协调多个线程的执行节奏。
3.1 CountDownLatch ------ 等待"门闩"打开
CountDownLatch 允许一个或多个线程等待其他线程完成各自的操作 。它有一个计数器,初始值为 N。每当一个线程完成了自己的任务,调用 countDown(),计数器减 1。当计数器减到 0 时,所有在 await() 上等待的线程被唤醒。
典型场景:一个服务启动时,需要等待多个前置组件初始化完成后再对外提供服务。
java
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int taskCount = 3;
CountDownLatch latch = new CountDownLatch(taskCount);
// 启动 3 个线程执行任务
for (int i = 1; i <= taskCount; i++) {
final int id = i;
new Thread(() -> {
try {
System.out.println("任务 " + id + " 开始执行...");
Thread.sleep((long) (Math.random() * 2000));
System.out.println("任务 " + id + " 执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 计数器减1
}
}).start();
}
System.out.println("主线程等待所有任务完成...");
latch.await(); // 阻塞,直到计数器变为0
System.out.println("所有任务已完成,主线程继续执行!");
}
}
CountDownLatch 是一次性的------计数器减到 0 后就不能再重置了。如果需要重复使用,请看下面的 CyclicBarrier。
3.2 CyclicBarrier ------ 循环屏障
CyclicBarrier 让一组线程互相等待 ,直到所有线程都到达某个"屏障点"后,才一起继续执行。它和 CountDownLatch 的区别是:
CountDownLatch是"等待其他线程完成",是一个线程等一组线程。CyclicBarrier是"所有线程互相等待",是一组线程互相等。CyclicBarrier可以重置 (reset()),可以重复使用。
典型场景:多个线程分段处理一个大数据集,每处理完一段后等待其他线程处理完,然后汇总。
java
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
int threadCount = 3;
// 所有线程到达屏障后,执行一个汇总动作(可选)
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
System.out.println("所有线程已到达屏障,执行汇总!");
});
for (int i = 1; i <= threadCount; i++) {
final int id = i;
new Thread(() -> {
try {
System.out.println("线程 " + id + " 开始工作...");
Thread.sleep((long) (Math.random() * 3000));
System.out.println("线程 " + id + " 到达屏障,等待其他线程...");
barrier.await(); // 等待其他线程
System.out.println("线程 " + id + " 越过屏障,继续执行!");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
如果某个线程在 await() 时被中断或被超时,屏障会被"破坏"(Broken),其他线程会抛出 BrokenBarrierException。这是一种"全员退出"的机制,避免一个线程出问题时其他线程永远等待。
3.3 Semaphore ------ 限流"信号量"
Semaphore(信号量)用来控制同时访问某个资源的线程数量 。它有一个许可(permit)计数,线程通过 acquire() 获取许可,如果许可不足则阻塞;通过 release() 释放许可。
典型场景:数据库连接池(最多 10 个连接)、限流器(限制 QPS)。
java
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
public static void main(String[] args) {
// 限制同时最多 3 个线程访问
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 10; i++) {
final int id = i;
new Thread(() -> {
try {
// 尝试获取许可(如果获取不到,阻塞)
semaphore.acquire();
System.out.println("线程 " + id + " 获得许可,开始工作...");
Thread.sleep((long) (Math.random() * 2000));
System.out.println("线程 " + id + " 工作完成,释放许可");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}).start();
}
}
}
Semaphore 也支持 tryAcquire()(尝试获取,不阻塞)和带超时的 tryAcquire(timeout, unit),非常灵活。
4. Fork/Join 框架 ------ 分治并行
Fork/Join 框架是 Java 7 引入的,专门用于可以递归分解的任务(分治算法)。它的核心思想是:
- Fork(分):把一个大任务拆分成若干个小任务。
- Join(合):把小任务的结果合并成大任务的结果。
典型的应用场景是并行计算,比如求一个巨大数组的和、快速排序的并行版本等。
java
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
public class ForkJoinDemo {
// 求 1 到 N 的和(用分治方式)
static class SumTask extends RecursiveTask<Long> {
private final long from;
private final long to;
private static final long THRESHOLD = 100_000; // 阈值:如果范围小于这个值,直接计算
SumTask(long from, long to) {
this.from = from;
this.to = to;
}
@Override
protected Long compute() {
if (to - from <= THRESHOLD) {
// 直接计算
long sum = 0;
for (long i = from; i <= to; i++) {
sum += i;
}
System.out.println(Thread.currentThread().getName() + " 计算 " + from + " ~ " + to + " = " + sum);
return sum;
} else {
// 分解任务
long mid = (from + to) / 2;
SumTask left = new SumTask(from, mid);
SumTask right = new SumTask(mid + 1, to);
// 使用 fork() 异步执行子任务
left.fork();
// 同步执行右任务
long rightResult = right.compute();
// 等待左任务完成并获取结果
long leftResult = left.join();
return leftResult + rightResult;
}
}
}
public static void main(String[] args) {
// 创建一个 ForkJoinPool
ForkJoinPool pool = new ForkJoinPool();
long n = 1_000_000L;
long result = pool.invoke(new SumTask(1, n));
System.out.println("1 到 " + n + " 的和 = " + result);
pool.shutdown();
}
}
ForkJoinPool 使用**工作窃取(Work-Stealing)**算法:每个线程都有自己的任务队列,如果某个线程完成了自己的任务,它会从其他线程的队列尾部"偷取"任务来执行,从而提高 CPU 利用率。
JDK 8 中 Arrays.parallelSort()、Stream.parallel() 等内部都使用了 Fork/Join 框架。
5. CompletableFuture ------ 异步编程的"瑞士军刀"
CompletableFuture 是 Java 8 引入的,它是对 Future 的增强,支持回调 和链式组合。它让异步编程变得极其灵活。
java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class CompletableFutureDemo {
public static void main(String[] args) throws Exception {
// 1. 创建异步任务
CompletableFuture.supplyAsync(() -> {
// 模拟耗时操作
sleep(1000);
return "Hello";
}).thenApply(result -> {
// 转换结果
return result + " World";
}).thenAccept(result -> {
// 消费结果
System.out.println("结果:" + result);
}).exceptionally(ex -> {
// 处理异常
System.err.println("出错了:" + ex.getMessage());
return null;
});
// 2. 组合多个 CompletableFuture
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
sleep(500);
return 10;
});
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
sleep(800);
return 20;
});
// 等待两个任务都完成,然后合并结果
CompletableFuture<Integer> combined = future1.thenCombine(future2, (a, b) -> a + b);
System.out.println("合并结果:" + combined.get()); // 30
// 等待任意一个完成(谁先完成用谁)
CompletableFuture<Object> any = CompletableFuture.anyOf(future1, future2);
System.out.println("先完成的:" + any.get());
// 注意:主线程不要过早退出,否则异步任务可能没执行完
Thread.sleep(2000);
}
private static void sleep(int ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
CompletableFuture 支持的方法非常丰富,包括 thenApply(转换)、thenAccept(消费)、thenRun(执行Runnable)、thenCombine(合并)、anyOf(任意一个完成)、allOf(全部完成)等。它让 Java 的异步编程体验接近了 JavaScript 的 Promise。
6. 综合示例 ------ 并行下载器
让我们用今天学到的工具,实现一个简单的"并行下载器"模拟程序:同时下载多个文件,等待所有下载完成后再统一处理。
java
import java.util.concurrent.*;
import java.util.*;
public class ParallelDownloader {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 模拟文件列表
List<String> files = Arrays.asList(
"file1.zip", "file2.zip", "file3.zip", "file4.zip", "file5.zip"
);
// 使用线程池(每个文件一个线程并行下载)
ExecutorService executor = Executors.newFixedThreadPool(3);
// 用 CompletableFuture 处理异步下载
List<CompletableFuture<String>> futures = files.stream()
.map(file -> CompletableFuture.supplyAsync(() -> downloadFile(file), executor))
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
// 等待所有下载完成,用 allOf
CompletableFuture<Void> allDone = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
// 所有下载完成后,汇总结果
CompletableFuture<List<String>> resultFuture = allDone.thenApply(v ->
futures.stream()
.map(CompletableFuture::join) // 这里 join 不会阻塞,因为都已经完成了
.collect(Collectors.toList())
);
// 打印结果
System.out.println("所有下载任务已提交,等待完成...");
List<String> results = resultFuture.get();
System.out.println("下载完成!");
results.forEach(System.out::println);
executor.shutdown();
}
private static String downloadFile(String filename) {
try {
// 模拟下载耗时(随机 1~3 秒)
long time = (long) (Math.random() * 2000 + 1000);
Thread.sleep(time);
System.out.println(Thread.currentThread().getName() + " 下载 " + filename + " 完成,耗时 " + time + "ms");
return filename + " (大小: " + (time / 100) + "KB)";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return filename + " 下载失败";
}
}
}
这个例子综合运用了:线程池、CompletableFuture、allOf、join,代码简洁且功能完整。
7. 今天的总结
今天我们学习了 JUC 包中的几个核心并发工具:
| 工具 | 用途 |
|---|---|
ExecutorService(线程池) |
管理线程生命周期,复用线程。核心:Executors 工厂方法、submit()、Future |
BlockingQueue(阻塞队列) |
线程安全的队列,自带等待通知。核心:put()、take() |
CountDownLatch(门闩) |
一个/多个线程等待其他线程完成后再执行 |
CyclicBarrier(屏障) |
一组线程互相等待,全部到达屏障点后一起继续 |
Semaphore(信号量) |
控制同时访问某个资源的线程数量 |
ForkJoinPool(分治框架) |
递归分解任务,并行计算后合并结果 |
CompletableFuture(异步增强) |
支持回调、链式组合的 Future |
这些工具让并发编程从"手工织布"变成了"使用现代化纺织机"。你不需要再自己管理线程池的细节,不需要再手动写 wait()/notify() 来实现生产者-消费者,不需要再费心思去设计线程协调的逻辑。JUC 已经帮你做好了。
动手试试
-
用
ThreadPoolExecutor显式构造一个线程池(核心线程 2,最大线程 4,队列容量 10),提交 20 个任务,观察哪些任务被拒绝,哪些被丢弃(根据拒绝策略不同)。 -
用
BlockingQueue实现一个简单的"日志队列":多个线程往队列里写日志,一个线程从队列里消费日志并打印(模拟异步日志)。 -
用
CountDownLatch实现一个"并发测试工具":同时启动 10 个线程,让它们在同一个"起跑线"上开始执行(提示:用CountDownLatch(1)作为"起跑信号",所有线程await(),主线程countDown()后所有线程同时启动)。 -
用
CompletableFuture实现一个"旅游路线推荐器":并行查询三个平台的酒店价格(模拟耗时 1~3 秒),取最低价返回。 -
(挑战)用
ForkJoinPool实现一个并行快速排序(ParallelQuickSort),对一个大型整数数组进行排序,并和Arrays.sort()比较性能。
JUC 工具是 Java 并发编程的"标准库"。学会它们,你就能用更少的代码写出更高效、更安全的并发程序。
我们下一篇会深入 JVM 内存模型和可见性问题,理解 volatile 关键字和 happens-before 原则,这是并发编程的"内功心法"。
我们下一篇见。😃
📌 获取本系列示例代码请访问 GitCode。