实现一个简单的并发度控制执行器

并发限制执行器: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数量,resulti对应tasksi

核心优势

  • 精确控制并发度,避免下游限流
  • 立即返回Future,支持流式处理(边查边展示)
  • 资源利用率高,短任务不等待长任务
  • 代码简洁,无需手动管理信号量

如果这篇文章对你有帮助,欢迎关注我,持续分享高质量技术干货,助你更快提升编程能力。

相关推荐
Penge6663 小时前
Go 接口编译期断言
后端
我是一颗柠檬3 小时前
【MySQL全面教学】MySQL面试高频考点汇总Day15(2026年)
数据库·后端·mysql·面试
橙淮3 小时前
并发编程(六)
java·jvm
拽着尾巴的鱼儿3 小时前
springboot openfeign 自定义feign 接口重试机制
java·spring boot·后端
白露与泡影3 小时前
2026大厂Java面试题大全!牛客网最新版
java·开发语言
Ceelog3 小时前
久坐党自救指南:屏幕前 8 小时,身体到底在经历什么
前端·后端
EntyIU4 小时前
JVM内存与GC笔记
java·jvm·笔记
XS0301065 小时前
并发编程 六
java·后端
yaoxin5211235 小时前
419. 现代 Java IO 最佳实践 - 写入文本文件
java·windows·python
雪宫街道5 小时前
synchronized 锁的范围:对象锁、类锁与代码块锁
java·jvm·后端·面试