Java-Executor线程池配置-案例2

for循环查询数据表中deleted=1的已删除文件名字,每次分页查询查1000条,1000条任务交给线程池去完成对文件的并行物理删除,要求1000个任务全部执行完主线程再去查询下一批次的1000条数据。

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.*;

@Configuration(proxyBeanMethods = false)
public class AppManageThreadPoolConfig {

  // destroyMethod = "shutdown"其实可以去除,spring容器关闭时会对有shutdown()的资源自动调用shutdowen
  @Bean(name = "appManageThreadPool", destroyMethod = "shutdown")
  public ExecutorService getThreadPool() {
    int cpuCores = Runtime.getRuntime().availableProcessors();
    int corePoolSize = cpuCores * 2; // 删除文件是IO密集型任务
    int maxPoolSize = corePoolSize * 2; // 最大线程数 = corePoolSize * 2 / corePoolSize
    int queueCapacity = 800; // 有界队列容量
    ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 默认线程工厂,线程名为固定的 "pool-[poolNumber]-thread-[threadNumber]"格式,如pool-1-thread-1
    return new ThreadPoolExecutor(
            corePoolSize,
            maxPoolSize,
            60L, TimeUnit.SECONDS, // 线程空闲存活时间
            new ArrayBlockingQueue<>(queueCapacity), // 有界队列(默认非公平队列,可通过添加第二个参数true设置为公平队列,公平队列会牺牲性能)
            threadFactory,
            new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时的拒绝策略:当线程数达到maxPoolSize并且队列达到queueCapacity上限时,新任务由提交它的线程(主线程)执行
            // CallerRunsPolicy的设计精髓在于其负反馈调节机制。当它开始将任务回退给主线程执行时,会产生两个关键效果:
            // 1.降低提交速度:由于主线程需要亲自执行被拒绝的任务(调用任务的 run方法,是同步操作),它提交新任务到线程池的循环自然会变慢甚至被暂时阻塞。这相当于从源头降低了新任务涌入线程池的速度,为线程池中的工作线程争取了时间来处理队列中积压的任务
            // 2.避免资源耗尽:这是一种比无限制地堆积任务更优的策略。它通过让调用者付出"代价"(亲自运行任务),来保护整个系统不会因为任务无限增长而导致内存溢出(如果使用无界队列)或彻底崩溃
            // 尽管有上述机制,如果任务处理时间非常长,或者持续有超大量的任务涌入,主线程长时间忙于执行任务确实可能影响其原有的职责(比如接收用户请求或事件),适当增大队列容量可以在一定程度上延缓拒绝策略的触发,但需权衡内存占用和任务延迟
    );
  }
}
java 复制代码
@Resource
CompRevisionService revisionService;
@Resource
ComponentUploadService uploadService;

/**
 * 清理无效应用
 */
@GetMapping("/cleanInvalidApp")
@ResponseBody
@InterfaceLog
public Result cleanInvalidApp() {
    final int BATCH_SIZE = 1000;
    try {
        for (int page = 0; ; page++) {
            int offset = page * BATCH_SIZE;
            // LIMIT指定返回行数,OFFSET指定跳过的行数,Mysql中LIMIT 20, 10 等同于 LIMIT 10 OFFSET 20
            List<String> batchFileNameList = revisionService.getInvalidAppFileNameBatch(BATCH_SIZE, offset);
            // 如果当前批次没有数据,跳出循环
            if (CollectionUtils.isEmpty(batchFileNameList)) {
                break;
            }
            int batchNum =  page + 1;
            log.info("第 {} 批次, 需处理 {} 个文件", batchNum, batchFileNameList.size());

            // 多线程处理该批次文件,该批次全部处理完再处理下一批
            boolean batchCompleted = uploadService.parallelDeleteFiles(batchFileNameList, batchNum, batchFileNameList.size());

            if (!batchCompleted) {
                String eMsg = "第 " + batchNum + " 批次处理出现异常,停止后续批次的处理";
                log.warn(eMsg);
                throw new ApiException(ErrorCode.B0.code(), eMsg);
            }

            // 如果当前批次数据量小于分页大小,说明已是最后一页,跳出循环
            if (batchFileNameList.size() < BATCH_SIZE) {
                break;
            }
        }
        return Result.newSuccess("清理完成!", null);
    } catch (Exception e) {
        String eMsg = "清理出错!";
        if (e instanceof AssertUtil.BusinessException) {
            eMsg = e.getMessage();
        }
        log.error(LogUtil.formatErrorMessage(ErrorCode.B0, eMsg), e);
        throw new ApiException(ErrorCode.B0.code(), eMsg, e);
    }
}
java 复制代码
//注入线程池
@Resource(name = "appManageThreadPool")
private ExecutorService executorService;

@Override
public boolean parallelDeleteFiles(List<String> fileNameList, int batchNum, int fileNum) {
    Path basePath = Paths.get(applicationCompressedPath);
    if (!Files.exists(basePath)) {
        throw new AssertUtil.BusinessException("错误:基础目录不存在 - " + applicationCompressedPath);
    }

    log.info("开始处理第 {} 批文件,该批次总计需删除 {} 个文件", batchNum, fileNum);

    AtomicInteger successCount = new AtomicInteger(0);
    AtomicInteger notExistCount = new AtomicInteger(0);
    AtomicInteger failCount = new AtomicInteger(0);
    AtomicInteger totalProcessed = new AtomicInteger(0);

    // 使用CountDownLatch等待所有任务完成,确保每个批次完全处理完成后再继续下一批次
    CountDownLatch latch = new CountDownLatch(fileNameList.size());
    boolean batchSuccess = true;

    try {
        for (String fileName : fileNameList) {
            executorService.submit(() -> {
                try {
                    // 安全地构建文件路径,防止路径遍历攻击
                    Path filePath = basePath.resolve(fileName).normalize();

                    // 验证路径安全性,确保文件在基础目录内
                    if (!filePath.startsWith(basePath)) {
                        log.error("路径遍历攻击尝试: {}", fileName);
                        failCount.incrementAndGet();
                        return;
                    }

                    if (!Files.exists(filePath)) {
                        log.debug("文件不存在,跳过: {}", fileName);
                        notExistCount.incrementAndGet();
                        return;
                    }

                    // 执行删除
                    Files.delete(filePath);
                    successCount.incrementAndGet();
                    log.debug("成功删除: {}", fileName);

                } catch (IOException e) {
                    log.error("删除失败: {} - {}", fileName, e.getMessage());
                    failCount.incrementAndGet();
                } catch (Exception e) {
                    log.error("处理文件时发生意外错误: {} - {}", fileName, e.getMessage());
                    failCount.incrementAndGet();
                } finally {
                    totalProcessed.incrementAndGet();
                    latch.countDown(); // 标记任务完成 此时工作线程不会被阻塞,可以继续执行其他任务或进入空闲状态。而主线程则在await()处等待,直到所有工作线程都完成任务、计数器归零后才被唤醒继续执行
                }
            });
        }

        // 主线程在此等待1000个子任务全部完再继续执行,设置超时时间避免无限等待
        // 如果在这9分钟内,计数器成功递减到零(所有要删除的文件都处理完毕了),latch.await方法返回 true,表示等待成功
        // 如果等待了9分钟后,计数器仍然大于零(还有文件没删完),latch.await方法将停止等待并返回 false,表示超时
        if (!latch.await(9, TimeUnit.MINUTES)) { // nginx超时时间为10分钟,为了让前端拿到响应,这里设置为9分钟
            log.error("第 {} 批次文件删除操作超时", batchNum);
            batchSuccess = false;

            // 在超时发生时,并未尝试中断那些尚未完成的任务,一种改进思路是,在latch.await超时后,中断仍在运行的任务
        }
    } catch (InterruptedException e) { // 若阻塞的主线程(latch.await会让主线程阻塞)被外部设置了中断标记,await()会抛出该异常并让主线程退出阻塞
        log.error("主线程在等待该批次任务全部完成时被中断(即本批次的处理流程因被中断而未完整地同步等待至结束)!" +
                "设置该批次处理失败标记(其实该批次未必失败,因为工作线程组仍在继续运行),主线程在获取这个失败标记后会抛出ApiException异常,放弃后续批次的处理"
                , e);
        Thread.currentThread().interrupt();
        // 设置该批次处理失败标记(其实该批次未必失败,因为工作线程组仍在继续运行),主线程在获取这个失败标记后会抛出ApiException异常,放弃后续批次的处理
        batchSuccess = false;
    }

    // 输出最终统计结果
    int total = totalProcessed.get(); // 实际已处理的数量
    int pendingOnTimeout = fileNameList.size() - total; // 计算因超时尚未得到处理结果的数量
    log.info("第 {} 批次删除操作完成!总计需处理: {} 个文件;实际已处理: {} 个文件,其中成功: {} 个,失败: {} 个,不存在: {} 个。{}",
            batchNum,
            fileNameList.size(),
            total,
            successCount.get(),
            failCount.get(),
            notExistCount.get(),
            pendingOnTimeout > 0 ? "(超时时刻,尚有 " + pendingOnTimeout + " 个文件正在处理!)" : "");

    return batchSuccess && (failCount.get() == 0);
}
相关推荐
小张快跑。1 小时前
【Java企业级开发】(十)SpringBoot框架+项目实践
java·数据库·spring boot
夏小花花1 小时前
<editor> 组件设置样式不生效问题
java·前端·vue.js·xss
weixin_307779131 小时前
Jenkins Ioncions API 插件:现代化图标库在持续集成中的应用
java·运维·开发语言·前端·jenkins
AnAnCode1 小时前
【时间轮算法】时间轮算法的详细讲解,从基本原理到 Java 中的具体实现
java·开发语言·算法·时间轮算法
Java天梯之路1 小时前
Spring IOC 核心源码解析
java·spring·源码
JIngJaneIL1 小时前
基于Java二手交易管理系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot
ULTRA??1 小时前
C++类型和容器在MoonBit中的对应关系整理
开发语言·c++·rust
李白同学1 小时前
C++:queue、priority_queue的使用和模拟实现
开发语言·c++
雨中飘荡的记忆1 小时前
Spring Data JPA详解
java·spring