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

并发限制执行器: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,支持流式处理(边查边展示)
  • 资源利用率高,短任务不等待长任务
  • 代码简洁,无需手动管理信号量

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

相关推荐
Spring AI学习2 小时前
Spring AI深度解析(11/50):异常处理与容错机制实战
java·人工智能·spring
小兔崽子去哪了2 小时前
机器学习,KNN 算法
后端·python·机器学习
初次攀爬者2 小时前
知识库-向量化功能-文本文件向量化
后端
Java水解2 小时前
MySQL索引分析以及相关面试题
后端·mysql·面试
qq_12498707532 小时前
基于协同过滤算法的在线教育资源推荐平台的设计与实现(源码+论文+部署+安装)
java·大数据·人工智能·spring boot·spring·毕业设计
总是学不会.2 小时前
[特殊字符] 自动分区管理系统实践:让大型表维护更轻松
java·后端·数据库开发·开发
大筒木老辈子2 小时前
C++笔记---并发支持库(future)
java·c++·笔记
全靠bug跑2 小时前
Sentinel 服务保护实战:限流、隔离与熔断降级详解
java·sentinel
五岳2 小时前
Web层接口通用鉴权注解实践(基于JDK8)
java