Java使用Future实现局部多线程业务

一、业务背景

请求开始 —》批量生成PDF文件(耗时较长)—》将PDF文件汇总回写给第三方—》请求结束

        纵观整个业务背景,对于整个请求的耗时影响因素有很多,需要生成的PDF文件数量、需要合并的文件数量、生成与合并文件的耗时、回写逻辑与回写通信耗时等等。

        使用常规的串行处理势必将整体耗时拉满,使用传统异步又无法获取各线程执行结果,于是考虑使用Future设计模式的半异步处理,也可以理解为局部异步处理。

二、设计思路

        根据业务背景分析,将批量生成PDF文件部分进行异步处理可以大幅提升接口响应,其余部分仍然串行运行即可。

        假设本次有十份PDF文件需要生成,平均每份生成速度为 1s 。如果串行执行十份就需要10s,如果阻塞式异步执行十份PDF生成可能只需要1.5s - 2s 之间。

请求开始—》异步生成PDF文件—》监听Futures全部执行成功开始回写第三方—》请求结束

三、代码实现

        先上完整代码,文章后面进行重点部分的解释

package com.aikes.mdap.common;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

@RestController
@RequestMapping("aikes")
@Slf4j
public class AsyncController {

    private Executor asyncExecutor = getAsyncExecutor();


    // 模拟生成PDF文件(返回PDF路径)
    public String generatePdf(int taskId) {
        try {
            // 模拟耗时操作(0.5~2秒随机时间)
            int sleepTime = new Random().nextInt(1500) + 500;
            Thread.sleep(sleepTime);

            String pdfPath = "/tmp/report_" + taskId + ".pdf";
            log.info(Thread.currentThread()+"——生成PDF成功: " + pdfPath + " [耗时: " + sleepTime + "ms]");
            return pdfPath;
        } catch (Exception e) {
            throw new RuntimeException("生成PDF失败", e);
        }
    }

    // 模拟HTTP回写(汇总所有PDF路径)
    public void callbackPdfs(List<String> pdfPaths) {
        log.info(Thread.currentThread()+"——开始回写PDF: " + pdfPaths);
        try {
            // 模拟耗时操作(0.5~2秒随机时间)
            int sleepTime = new Random().nextInt(1500) + 500;
            Thread.sleep(sleepTime);
            log.info(Thread.currentThread()+"——回写成功!耗时" + sleepTime);
        } catch (Exception e) {
            throw new RuntimeException("回写失败", e);
        }
    }

    /**
     * 自定义线程池
     */
    public Executor getAsyncExecutor() {
        if(asyncExecutor!=null) return asyncExecutor;
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(20); //核心线程数
        executor.setMaxPoolSize(50);  //最大线程数
        executor.setQueueCapacity(1000); //队列大小
        executor.setKeepAliveSeconds(300); //线程最大空闲时间
        executor.setThreadNamePrefix("pdfAsync-Executor-"); //指定用于新创建的线程名称的前缀。
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        asyncExecutor = executor;
        return executor;
    }

    @RequestMapping(value = "/testAsyncPdf", method = RequestMethod.POST)
    public Object testAsyncPdf() throws Exception{
        // 1. 记录开始时间
        long startTime = System.currentTimeMillis();

        // 2. 创建10个异步任务(每个任务生成一个PDF)
        //使用Stream流
//        List<CompletableFuture<String>> futures = IntStream.range(1, 11)
//                .mapToObj(taskId ->
//                        CompletableFuture.supplyAsync(() -> generatePdf(taskId), asyncExecutor)
//                )
//                .collect(Collectors.toList());
        //使用常规循环
        List<CompletableFuture<String>> futures = new ArrayList<>();
        for (int taskId = 1; taskId <= 10; taskId++) {
            final int currentTaskId = taskId;
            CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> generatePdf(currentTaskId), asyncExecutor);
            futures.add(future);
        }

        // 3. 合并所有任务结果
        CompletableFuture<Void> allDone = CompletableFuture.allOf((futures.toArray(new CompletableFuture[0])));

        // 4. 所有任务完成后,汇总PDF路径并回写
        CompletableFuture<List<String>> result = allDone.thenApply(v ->
                futures.stream()
                        .map(CompletableFuture::join) // 获取每个任务的结果
                        .collect(Collectors.toList())
        );
        List<String> strings = result.get();//获取返回结果
        String tResult = JSON.toJSONString(strings);

        // 5. 调用回写接口
        result.thenAccept(this::callbackPdfs);

        log.info((Thread.currentThread() + "——总耗时: " + (System.currentTimeMillis() - startTime) + "ms"));
        return tResult;
    }

}

执行结果:

        根据日志可以看到,整个接口的响应基本是两个红框耗时的累加,一个是PDF生成的最长耗时,另一个是回写耗时,这与我们期望的效果是完全一致的,整体响应对比串行优化了很多。

        接下来是关于代码中的一些重点解释:

        1、多线程调用生成PDF时,我们获取到多个CompletableFuture对象,然后创建一个新的CompletableFuture将前面的多个Future整合到一起,作为后续监测一起完成时使用。

        新的allDone对象的thenApply是一个回调方法,当主线程执行到此刻如果Future没有全部完成时是不会执行,主线程会继续向下,如果没有同步阻塞方法会直接执行完毕,上述示例中是因为后面有一行获取result的代码:

List<String> strings = result.get();//获取返回结果

        从而导致主线程在此阻塞,等待Future全部返回才会继续执行,大家可以测试下如果接口不返回PDF路径(即:不调用result.get方法),接口响应速度甚至趋向于 0 ms,因为生成和回调都不存在阻塞,所以主线程并没有太多逻辑。

        也就是说allDone.thenApply和result.thenAccept都是不阻塞的方法,只有在执行到result.join或者result.get才会阻塞等待。

        2、关于lambda表达式笔者研究不深,所以有些地方更喜欢用for循环处理,总感觉这样更直观。上述代码在异步生成文件处就有两套代码,大家根据自己习惯使用就好,我们接受新鲜事物(虽然也不是很新...)但习惯这东西还真是难改~

        3、关于线程池需要留意,重点看日志打印是否真的多个线程处理,平时开发过程中总是一看 Async关键线程名就觉得是异步了,实际好多时候都是 Async-1 。这种时候大概率是每次创建了新的线程池,而不是从一个线程池里取线程,后续存在线程泄露的风险。

四、总结

        关于Future异步编程本文只讲述了冰山一角,这种声明式代码回调执行需要逐步适应,如果有前端开发经验可能会更好理解,类似钩子函数需要特定场景触发再执行。另外,不是所有业务都试用,还是要根据业务场景进行权衡选择,所有的技术手段都是因业务的多样性而逐步创新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aikes902

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值