利用线程池 + FutureTask 实现并发查询

5 阅读3分钟

一、场景背景

在实际业务开发中,查询分类及关联标签是非常常见的业务场景:

  1. 先查询一级 / 二级分类列表
  2. 遍历每个分类,查询其关联的标签数据
  3. 封装结果返回给前端

如果使用串行遍历查询:分类数量越多,接口响应时间越长,性能极差。 因此,本文采用自定义线程池 + FutureTask 异步并发查询方案,大幅提升接口响应速度,同时保证代码优雅性与可维护性。

二、使用方案:

  • 线程池:自定义 ThreadPoolExecutor,复用线程,避免频繁创建销毁线程带来的开销
  • FutureTask:支持带返回值的异步任务,可并发执行并统一获取结果
  • 并发查询:并行查询每个分类的标签数据,而非串行执行
  • 结果聚合:统一收集异步任务结果,封装成前端需要的数据结构

三、代码实现

1. 自定义线程池配置

首先定义线程池,交给 Spring 管理,控制并发线程数、队列、拒绝策略。

/** 
* 线程池配置类 
* 用于分类标签并发查询
*/ 
@Configuration 
public class ThreadPoolConfig { 
/** 
* 标签查询专用线程池 
*/ 
    @Bean(name = "labelThreadPool")
    public ThreadPoolExecutor getLabelThreadLocal() { 
        return new ThreadPoolExecutor( 
        // 核心线程数 
        20, 
        // 最大线程数 
        100, 
        // 空闲线程存活时间 
        5, 
        // 时间单位 
        TimeUnit.SECONDS, 
        // 阻塞队列容量40 
        new LinkedBlockingDeque<>(40), 
        // 默认线程工厂 
        Executors.defaultThreadFactory(), 
        // 拒绝策略:调用者线程执行 
        new ThreadPoolExecutor.CallerRunsPolicy() 
        ); 
    } 
}

线程池参数说明

参数说明
核心线程数20长期存活的线程数
最大线程数100并发峰值最大线程数
空闲时间5s超出核心线程的线程空闲存活时间
阻塞队列40任务排队队列
拒绝策略CallerRunsPolicy队列 / 线程满后,由提交任务的主线程执行,保证不丢失任务

2. 业务层:并发查询分类 + 标签

核心业务逻辑:

  1. 查询全部分类
  2. 为每个分类创建一个 FutureTask 异步任务
  3. 提交到线程池并发执行
  4. 遍历 FutureTask 获取所有结果并聚合
  5. 封装最终结果返回
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.web.bind.annotation.Resource;

import java.util.*;
import java.util.concurrent.FutureTask;
import java.util.stream.Collectors;

/**
 * 分类标签业务实现类
 * 线程池并发查询优化
 */
@Slf4j
@Service
public class SubjectCategoryServiceImpl implements SubjectCategoryService {

    // 注入自定义线程池
    @Resource
    private ThreadPoolExecutor labelThreadPool;

    // 其他Service依赖
    @Resource
    private SubjectCategoryService subjectCategoryService;
    @Resource
    private SubjectMappingService subjectMappingService;
    @Resource
    private SubjectLabelService subjectLabelService;

    /**
     * 查询分类及关联标签(并发优化版)
     */
    @Override
    @SneakyThrows
    public List<SubjectCategoryBO> queryCategoryAndLabel(SubjectCategoryBO subjectCategoryBO) {
        // 1. 查询当前大类下的所有分类
        SubjectCategory subjectCategory = new SubjectCategory();
        subjectCategory.setParentId(subjectCategoryBO.getId());
        subjectCategory.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode());
        List<SubjectCategory> subjectCategoryList = subjectCategoryService.queryCategory(subjectCategory);

        // 实体转换
        List<SubjectCategoryBO> categoryBOList = SubjectCategoryConverter.INSTANCE
                .convertCategoryToBoList(subjectCategoryList);

        if (CollectionUtils.isEmpty(categoryBOList)) {
            return Collections.emptyList();
        }

        // 2. 创建FutureTask集合,用于并发查询标签
        List<FutureTask<Map<Long, List<SubjectLabelBO>>>> futureTaskList = new LinkedList<>();

        // 3. 遍历分类,为每个分类创建异步任务
        categoryBOList.forEach(category -> {
            FutureTask<Map<Long, List<SubjectLabelBO>>> futureTask = new FutureTask<>(() -> getLabelBOList(category));
            futureTaskList.add(futureTask);
            // 提交至线程池执行
            labelThreadPool.submit(futureTask);
        });

        // 4. 统一获取所有异步任务结果
        Map<Long, List<SubjectLabelBO>> resultMap = new HashMap<>();
        for (FutureTask<Map<Long, List<SubjectLabelBO>>> futureTask : futureTaskList) {
            Map<Long, List<SubjectLabelBO>> map = futureTask.get();
            if (CollectionUtils.isNotEmpty(map)) {
                resultMap.putAll(map);
            }
        }

        // 5. 封装标签数据到分类BO中
        categoryBOList.forEach(bo -> bo.setLabelBOList(resultMap.get(bo.getId())));

        return categoryBOList;
    }

    /**
     * 查询单个分类下的标签(异步执行的方法)
     */
    private Map<Long, List<SubjectLabelBO>> getLabelBOList(SubjectCategoryBO category) {
        // 查询分类-标签映射关系
        SubjectMapping mapping = new SubjectMapping();
        mapping.setCategoryId(category.getId());
        mapping.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode());
        List<SubjectMapping> mappingList = subjectMappingService.queryLabelId(mapping);

        if (CollectionUtils.isEmpty(mappingList)) {
            return null;
        }

        // 提取标签ID
        List<Long> labelIdList = mappingList.stream()
                .map(SubjectMapping::getLabelId)
                .collect(Collectors.toList());

        // 批量查询标签
        List<SubjectLabel> labelList = subjectLabelService.batchQueryById(labelIdList);

        // 转换BO
        List<SubjectLabelBO> labelBOList = new LinkedList<>();
        labelList.forEach(label -> {
            SubjectLabelBO labelBO = new SubjectLabelBO();
            labelBO.setId(label.getId());
            labelBO.setLabelName(label.getLabelName());
            labelBO.setCategoryId(label.getCategoryId());
            labelBO.setSortNum(label.getSortNum());
            labelBOList.add(labelBO);
        });

        // 封装成Map,方便聚合
        Map<Long, List<SubjectLabelBO>> resultMap = new HashMap<>();
        resultMap.put(category.getId(), labelBOList);
        return resultMap;
    }
}

四、代码核心知识点讲解

1. FutureTask 作用

  • 用于异步执行任务并获取返回值
  • 实现Callable,可以返回结果
  • 配合线程池实现并发
FutureTask<Map<Long,List<SubjectLabelBO>>> futureTask = new FutureTask<>(()-> getLabelBOList(category));

2. 并发执行流程

  1. 分类查询 → 得到分类列表
  2. 每个分类创建一个独立异步任务
  3. 批量提交线程池,并行查询标签
  4. 调用futureTask.get()获取结果
  5. 合并所有结果,封装返回

3. 线程池优势

  • 避免为每个请求创建大量线程
  • 控制并发数,防止压垮数据库
  • 线程复用,减少资源消耗