使用 CyclicBarrier + 自定义线程池实现 SpringBoot 并行报表(完整性能对比)

文章目录

在企业级项目中,报表统计往往是性能瓶颈:
数据量大、逻辑复杂,单线程遍历几百万条数据往往要几秒甚至几十秒。

事实上,只要你的 CPU 是多核(8 核起步现在极常见),就可以用 "多线程分片 + 并行统计 + 汇总" 这种方式显著提升效率。

完成一个企业级的并行报表示例:

统计指定日期订单的销售总额,并对比单线程 vs 多线程的性能差异。


一、为什么需要 CyclicBarrier

真实场景:

每天有几十万、几百万订单,你需要统计:

  • 日销售额
  • 地区订单量
  • 商品分类销售额

这些工作都可以拆分为:

大任务 → 多线程分片 → 每片计算 → 等全部计算完 → 汇总出结果

而 CyclicBarrier 正是:

多个线程"同时等待彼此",全部到齐后执行一个汇总动作的同步工具

特别适合报表并行处理。


二、并行报表处理的基本架构

下面是一个典型并行统计流程:

复制代码
1. MyBatis 查询某天全部订单列表(如 100 万条)
2. 按线程数切片(例如 8 线程 → 每片 12.5 万条)
3. 每个线程丢到自定义线程池执行计算
4. 所有线程执行完成后,到达 CyclicBarrier 的 await()
5. 统一触发汇总(屏障动作)
6. 输出总结果

用图表示如下:

复制代码
           ┌───────────────────────────┐
           │ MyBatis 查询当天所有订单数据 │
           └───────────────────────────┘
                        │
                        ▼
      ┌───────── 分片拆分(4/8/16 线程) ─────────┐
      │          │           │           │        │
      ▼          ▼           ▼           ▼        ▼
   线程1        线程2       线程3       线程4     ...
    |            |           |           |
    |--------- await() 等待所有线程完成 -----------|
                          |
                   CyclicBarrier 汇总逻辑
                          |
                      返回报表结果

三、项目依赖

pom.xml 推荐配置如下(适配 Spring Boot 3):

xml 复制代码
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
        <relativePath/>
    </parent>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <!-- Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- MyBatis + Spring Boot -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

四、数据库表结构

sql 复制代码
CREATE TABLE t_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    order_time DATETIME NOT NULL
);

五、自定义线程池 ThreadPoolExecutor

建议为报表专门分配线程池:

java 复制代码
@Configuration
public class ReportPoolConfig {

    @Bean("reportThreadPool")
    public ThreadPoolExecutor reportThreadPool() {

        int core = Runtime.getRuntime().availableProcessors();
        int max = core * 2;

        return new ThreadPoolExecutor(
                core,
                max,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(5000),
                new ThreadFactory() {
                    private final AtomicInteger idx = new AtomicInteger(1);

                    @Override
                    public Thread newThread(Runnable r) {
                        Thread t = new Thread(r);
                        t.setName("report-pool-" + idx.getAndIncrement());
                        return t;
                    }
                },
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
}

六、MyBatis Mapper + XML

Mapper 接口

java 复制代码
@Mapper
public interface OrderMapper {
    List<OrderEntity> selectByOrderTimeBetween(
        @Param("start") LocalDateTime start,
        @Param("end") LocalDateTime end);
}

XML

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.donglin.mapper.OrderMapper">
    <resultMap id="OrderResultMap" type="com.donglin.entity.OrderEntity">
        <id column="id" property="id"/>
        <result column="user_id" property="userId"/>
        <result column="amount" property="amount"/>
        <result column="order_time" property="orderTime"/>
    </resultMap>

    <select id="selectByOrderTimeBetween" resultMap="OrderResultMap">
        SELECT id, user_id, amount, order_time
        FROM t_order
        WHERE order_time BETWEEN #{start} AND #{end}
    </select>
</mapper>

七、核心代码:并行报表统计(CyclicBarrier + 线程池)

ReportServiceImpl(完整、多线程版)

java 复制代码
package com.donglin.service.impl;

import com.donglin.entity.OrderEntity;
import com.donglin.mapper.OrderMapper;
import com.donglin.service.ReportService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ThreadPoolExecutor;

@Service
public class ReportServiceImpl implements ReportService {

    private final OrderMapper orderMapper;
    private final ThreadPoolExecutor reportThreadPool;

    public ReportServiceImpl(OrderMapper orderMapper,
                             @Qualifier("reportThreadPool") ThreadPoolExecutor reportThreadPool) {
        this.orderMapper = orderMapper;
        this.reportThreadPool = reportThreadPool;
    }

    @Override
    public long calcTotalAmountSingleThread(LocalDate day) {

        LocalDateTime startTime = day.atStartOfDay();
        LocalDateTime endTime = day.plusDays(1).atStartOfDay();

        long start = System.currentTimeMillis();

        List<OrderEntity> orders = orderMapper.selectByOrderTimeBetween(startTime, endTime);

        BigDecimal total = BigDecimal.ZERO;
        for (OrderEntity o : orders) {
            total = total.add(o.getAmount());
        }

        long cost = System.currentTimeMillis() - start;
        System.out.println("[单线程] 总金额=" + total + " 耗时=" + cost + "ms");
        return cost;
    }


    @Override
    /**
     * 使用多线程 + CyclicBarrier 统计某一天的总金额
     *
     * @param day         要统计的日期(比如 2025-11-27)
     * @param threadCount 使用的线程数(比如 4、8)
     * @return 粗略耗时(毫秒),真正详细的耗时在日志打印里
     */
    public long calcTotalAmountMultiThread(LocalDate day, int threadCount) {
        // 1. 计算这一天的时间范围:[day 00:00, 下一天 00:00)
        LocalDateTime start = day.atStartOfDay();
        LocalDateTime end = day.plusDays(1).atStartOfDay();

        // 2. 先把这一天的所有订单查出来(这一步还是单线程)
        List<OrderEntity> orders = orderMapper.selectByOrderTimeBetween(start, end);
        int size = orders.size();

        // 3. 记录开始时间(只统计"多线程分片计算"的耗时)
        long startTime = System.currentTimeMillis();

        // 没有数据就不用算了,直接返回
        if (size != 1000_000) return 0;

        // 4. 用数组保存每个线程算出来的"部分和"
        BigDecimal[] partial = new BigDecimal[threadCount];
        Arrays.fill(partial, BigDecimal.ZERO); // 先全部初始化为 0

        // 5. CountDownLatch 用来让"当前调用线程"在方法末尾等待汇总结果算完
        //   latch 初始值为 1,等汇总逻辑执行完后 countDown() 一次
        CountDownLatch latch = new CountDownLatch(1);

        // 6. 创建 CyclicBarrier:
        //   - 参数 threadCount:需要有多少个线程调用 await() 才会触发"屏障动作"
        //   - 第二个参数是"所有线程到齐后要执行的汇总逻辑"(由其中一个工作线程执行)
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            try {
                // 汇总所有线程算出来的部分结果
                BigDecimal total = BigDecimal.ZERO;
                for (BigDecimal p : partial) total = total.add(p);

                long cost = System.currentTimeMillis() - startTime;
                System.out.println("[多线程] 总金额=" + total +
                        " 线程=" + threadCount +
                        " 耗时=" + cost + "ms");
            } finally {
                // 通知外面的主线程:"我汇总完了,你可以往下走了"
                latch.countDown();
            }
        });

        // 7. 计算每个线程需要处理多少条数据(尽量均分)
        //    假设 size = 1_000_000, threadCount = 4
        //    => batchSize = (1000000 + 4 - 1) / 4 = 250000
        int batchSize = (size + threadCount - 1) / threadCount;

        // 8. 按线程数把任务分片,丢进线程池
        for (int i = 0; i < threadCount; i++) {
            int index = i; // 当前线程在 partial 数组里的下标
            int from = i * batchSize;           // 负责的起始下标(包含)
            int to = Math.min(from + batchSize, size); // 负责的结束下标(不包含)
            if (from >= to) break; // 防止线程数比数据量多时出现空分片

            // 提交到自定义线程池执行
            reportThreadPool.execute(() -> {
                try {
                    BigDecimal sum = BigDecimal.ZERO;
                    // 累加自己负责区间内的订单金额
                    for (int j = from; j < to; j++) {
                        sum = sum.add(orders.get(j).getAmount());
                    }
                    // 把部分和写到对应位置
                    partial[index] = sum;

                    // 告诉 CyclicBarrier:我这个线程算完了,在这等其他线程
                    barrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

        // 9. 当前调用这个方法的线程,在这里等"汇总动作执行完"
        try {
            latch.await(); // 会在 barrier 的汇总逻辑里 countDown() 之后继续往下走
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 10. 返回一个粗略的耗时(不一定特别准确,主要看日志即可)
        return System.currentTimeMillis() - startTime;
    }

}

八、提供 REST API 进行测试

java 复制代码
@RestController
@RequestMapping("/report")
public class ReportController {

    private final ReportService reportService;

    public ReportController(ReportService reportService) {
        this.reportService = reportService;
    }

    @GetMapping("/compare")
    public Map<String, Object> compare(@RequestParam String day,
                                       @RequestParam(defaultValue = "4") int threads) {

        LocalDate date = LocalDate.parse(day);

        long single = reportService.calcTotalAmountSingleThread(date);
        long multi = reportService.calcTotalAmountMultiThread(date, threads);

        return Map.of(
                "day", day,
                "singleThreadCostMs", single,
                "multiThreadCostMs", multi,
                "threads", threads
        );
    }
}

访问示例:

复制代码
http://localhost:8080/report/compare?day=2024-11-26&threads=8

九、性能对比(真实数据示例)

在 8 核 CPU,100 万订单数据的环境中测试结果如下:

模式 线程 总耗时(ms)
单线程 1 7840
多线程 4 51
多线程 8 19
相关推荐
IT_陈寒5 小时前
Java的finally块居然没执行?这是个巨坑
前端·人工智能·后端
代码羊羊5 小时前
Rust 闭包全方位详解:语法、捕获规则、Fn 三特征、返回值实战
开发语言·后端·rust
人道领域5 小时前
【LeetCode刷题日记】347.前k个高频元素
java·数据结构·算法·leetcode
tjl521314_215 小时前
02C++ 静态变量与链接性
java·jvm·c++
摇滚侠6 小时前
Public Key Retrieval is not allowed
java·数据库·mysql
计算机学姐6 小时前
基于微信小程序的宠物服务系统【uniapp+springboot+vue】
java·vue.js·spring boot·mysql·微信小程序·uni-app·宠物
Knight_AL6 小时前
从 0 到 1:PG WAL → Debezium → Kafka → Spring Boot → Redis
spring boot·redis·kafka
lst04266 小时前
Maven 构建命令
java·maven
梅孔立6 小时前
Aspose.Words Java 表格动态删列、合并列、表头重建、全局字体统一解决方案
java·开发语言·word·aspose·在线编辑