并发限制执行器:ExecutorCompletionService 与 ListenableFuture 实战
大家好,我是桦说编程。
本文深入解析如何使用 ExecutorCompletionService 和 Guava ListenableFuture 实现并发度可控的任务执行器,掌握生产级并发编程的核心技巧。
问题背景
在高并发场景下,我们可能面临这样的需求(仅为例子):
场景:火车票查询系统,用户输入北京到上海,需要查询100个车次的余票信息。
需求:
- 批量查询:并发调用100个车次的余票查询接口
- 限制并发度:12306限流,单个应用最多10个并发,超过会返回"请稍后再试"
- 尽快返回:不能等全部查完,需要立即返回Future供上层组合(边查边展示)
- 按序映射:返回的Future列表要和车次列表一一对应(G1-G100)
传统方案的痛点:
| 方案 | 问题 |
|---|---|
ExecutorService.invokeAll() |
阻塞等待所有任务完成,无法立即返回 |
CompletableFuture.allOf() |
无法控制并发度,100个请求同时发出触发限流 |
| 自己用 Semaphore | 代码复杂,需要手动管理信号量和异常 |
| 分批执行(10个一批) | 任务执行时间不均匀,短任务等待长任务,资源利用率低 |
优化方案:滑动窗口 - 初始提交10个,每完成一个立即补充下一个,保持10个并发。
代码实现
java
/**
* 并发限制执行器
* <p>
* 核心功能:控制任务并发度,采用滑动窗口策略
* <p>
* 知识点:
* 1. ExecutorCompletionService - 按完成顺序获取任务结果
* 2. ListenableFuture - Guava 可监听 Future
* 3. SettableFuture - 可手动设置结果的 Future
* 4. 滑动窗口并发控制 - 初始提交N个,每完成一个立即补充下一个
*
* @author 桦说编程
*/
@Value
public class ConcurrentLimitExecutor<V> {
ListeningExecutorService pool;
int parallelism;
BlockingQueue<Future<V>> q;
ExecutorCompletionService<V> cs = new ExecutorCompletionService<>(pool, q);
ListeningExecutorService submitter = MoreExecutors.listeningDecorator(
Executors.newSingleThreadExecutor());
/**
* 提交所有任务,立即返回Future列表
*
* @param tasks 待执行的任务列表
* @return Future列表,顺序与tasks一致
*/
@SuppressWarnings("unchecked")
public List<ListenableFuture<V>> submitAll(List<Callable<V>> tasks) {
if (tasks.isEmpty()) {
return ImmutableList.of();
}
// 创建结果占位符,数量等于任务数量
List<SettableFuture<V>> result = IntStream.range(0, tasks.size())
.mapToObj(__ -> SettableFuture.<V>create())
.collect(toImmutableList());
// 首批提交数量
int start = Math.min(tasks.size(), parallelism);
// 提交首批任务
for (int i = 0; i < start; i++) {
ListenableFuture<V> f = (ListenableFuture<V>) cs.submit(tasks.get(i));
linkFuture(f, result.get(i));
}
// 异步提交剩余任务
submitter.submit(() -> submitRemaining(tasks, result, start));
return (List<ListenableFuture<V>>) (List<?>) result;
}
/**
* 提交剩余任务(在独立线程中执行)
* <p>
* 流程:
* 1. 等待任意任务完成(cs.take())
* 2. 提交下一个任务
* 3. 将新任务的Future链接到result对应位置
* 4. 重复直到所有任务提交完
*/
@SneakyThrows
private void submitRemaining(List<Callable<V>> tasks,
List<SettableFuture<V>> result,
int start) {
int index = start;
int size = tasks.size();
while (index < size) {
// 阻塞等待任意任务完成(释放一个并发槽位)
cs.take();
// 提交下一个任务
ListenableFuture<V> f = (ListenableFuture<V>) cs.submit(tasks.get(index));
// 链接结果到对应位置
linkFuture(f, result.get(index));
index++;
}
}
private void linkFuture(ListenableFuture<V> from, SettableFuture<V> to) {
from.addListener(() -> to.setFuture(from), directExecutor());
}
}
关键设计点:
| 设计元素 | 说明 |
|---|---|
| result 数量 | 等于 tasks.size(),每个任务都有对应的 Future |
| linkFuture | 将 cs 返回的 Future 链接到 result 中对应位置 |
| 异步补充 | 使用 submitter 线程池异步提交剩余任务,不阻塞调用方 |
| 立即返回 | submitAll() 立即返回,上层可用 Futures.allAsList() 组合 |
核心知识点
1. ExecutorCompletionService
JDK提供的任务完成服务,核心能力是按完成顺序获取结果。
java
ExecutorService pool = Executors.newFixedThreadPool(10);
ExecutorCompletionService<Train> cs = new ExecutorCompletionService<>(pool);
// 提交任务
cs.submit(() -> queryTrain("G1"));
cs.submit(() -> queryTrain("G2"));
// 阻塞获取最先完成的结果(而不是提交顺序)
Future<Train> first = cs.take(); // 谁先完成就拿到谁
Future<Train> second = cs.take();
原理:
swift
┌─────────────────────────────────────────────┐
│ ExecutorCompletionService │
├─────────────────────────────────────────────┤
│ - executor: ExecutorService │
│ - completionQueue: BlockingQueue<Future> │
├─────────────────────────────────────────────┤
│ + submit(Callable): Future │
│ + take(): Future // 阻塞获取已完成的 │
│ + poll(): Future // 非阻塞获取 │
└─────────────────────────────────────────────┘
│
│ 任务完成时自动放入队列
▼
┌─────────────────────────────────────────────┐
│ BlockingQueue<Future<V>> │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Future│ │Future│ │Future│ ... │
│ └──────┘ └──────┘ └──────┘ │
│ 已完成 已完成 已完成 │
└─────────────────────────────────────────────┘
关键点:
- 提交任务时包装Future,添加完成回调
- 任务完成后自动将Future放入
completionQueue take()从队列获取,天然按完成顺序
2. Guava ListenableFuture
标准Future的增强版,支持回调监听。
java
// 标准Future:只能轮询或阻塞
Future<Train> future = executor.submit(() -> queryTrain("G1"));
Train result = future.get(); // 阻塞
// ListenableFuture:注册回调
ListeningExecutorService executor = MoreExecutors.listeningDecorator(pool);
ListenableFuture<Train> future = executor.submit(() -> queryTrain("G1"));
// 异步回调(不阻塞)
future.addListener(() -> {
System.out.println("G1查询完成!");
}, directExecutor());
// 或使用Futures工具类
Futures.addCallback(future, new FutureCallback<Train>() {
public void onSuccess(Train result) { ... }
public void onFailure(Throwable t) { ... }
}, executor);
核心价值:
- 非阻塞 :不需要轮询
isDone()或阻塞get() - 组合能力 :
Futures.allAsList(),transform(),catching()等 - 异常传播:失败会自动传播到下游Future
3. SettableFuture
可手动设置结果的Future,类似CompletableFuture。
java
SettableFuture<Train> future = SettableFuture.create();
// 在其他线程设置结果
future.set(train); // 正常完成
future.setException(new TimeoutException()); // 异常完成
future.setFuture(otherFuture); // 链接另一个Future
// 调用方正常使用
Train result = future.get();
4. 并发度控制核心思路
滑动窗口策略:
ini
假设 parallelism=3, tasks=10(查询10个车次)
初始状态:提交前3个车次到线程池
┌───┬───┬───┐ ┌───┬───┬───┬───┬───┬───┬───┐
│ G1│ G2│ G3│ │ G4│ G5│ G6│ G7│ G8│ G9│G10│
└───┴───┴───┘ └───┴───┴───┴───┴───┴───┴───┘
执行中 等待中
G1 完成 → 立即提交 G4
┌───┬───┬───┐ ┌───┬───┬───┬───┬───┬───┐
│ │ G2│ G3│ │ G4│ G5│ G6│ G7│ G8│ G9│G10│
└───┴───┴───┘ └───┴───┴───┴───┴───┴───┴───┘
执行中 等待中
G2 完成 → 立即提交 G5
...
维持窗口大小 = parallelism,任务完成后立即补充新任务
实现要点:
- 使用
ExecutorCompletionService.take()等待任意任务完成 - 每完成一个,立即从待执行队列提交下一个
- 保证同时执行的任务数 ≤ parallelism
使用场景
| 场景 | 说明 |
|---|---|
| 批量查询接口 | 火车票、机票、酒店查询,下游限流保护 |
| 批量缓存查询 | Redis 连接池有限,控制并发数 |
| 批量 IO 操作 | 文件读写、数据库查询 |
| 爬虫系统 | 限制爬取速率,避免被封 |
总结
- ExecutorCompletionService -
take()按完成顺序获取,支持动态补充任务 - ListenableFuture + SettableFuture - 通过
linkFuture将内部Future链接到返回占位符 - 双线程池设计 - pool执行任务,submitter异步补充,不阻塞调用方
- 滑动窗口策略 - 初始提交N个,每完成一个立即补充下一个
- 索引严格对应 - result数量等于tasks数量,result[i]对应tasks[i]
核心优势:
- 精确控制并发度,避免下游限流
- 立即返回Future,支持流式处理(边查边展示)
- 资源利用率高,短任务不等待长任务
- 代码简洁,无需手动管理信号量
如果这篇文章对你有帮助,欢迎关注我,持续分享高质量技术干货,助你更快提升编程能力。