Java线程池实战:高效并发编程技巧

概要

因为面试中暴露出来的不足,所以写一写线程池,也算是复习一下。


什么是线程池

线程池是一种线程管理机制,它预先创建一定数量的线程并放入池中,当需要执行任务时,从池中获取空闲线程来执行任务,任务完成后线程不销毁而是返回池中等待下一次任务。


主要作用

1、降低资源消耗

避免频繁创建和销毁线程的开销,重复利用已经创建好了的线程。

2、提高响应速度

任务到达时,无需额外创建线程即可运行

3、提高线程可管理性

统一管理线程资源,避免无限制创建线程导致系统崩溃,可以控制并发线程数量,避免过度竞争。

4、提供更强大的功能

  • 定时执行,周期执行
  • 任务队列管理
  • 拒绝策略

基础线程池使用实例

java 复制代码
import java.util.concurrent.*;
import java.util.Random;

public class ThreadPoolDemo {
    
    public static void main(String[] args) {
        // 1. 创建线程池
        // 核心参数:核心线程数5,最大线程数10,空闲时间60秒,任务队列容量100
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            5,  // corePoolSize
            10, // maximumPoolSize
            60, // keepAliveTime
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100), // 任务队列
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
        );
        
        // 2. 提交任务
        for (int i = 1; i <= 20; i++) {
            int taskId = i;
            executor.execute(() -> {
                System.out.println("处理任务" + taskId + 
                    ", 线程: " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // 模拟业务处理
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
        
        // 3. 优雅关闭
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
    }
}

如上所示,我们经历了:

1、创建线程池

其核心线程数为5,最大线程数为10,空闲时间60s,任务队列容量为100

2、提交任务

  • 循环提交 :代码通过 for 循环向线程池提交了 20 个任务。

  • 变量捕获 :这里定义 int taskId = i; 是因为在 Lambda 表达式内部引用的外部变量必须是 finaleffectively final (即不再改变)。直接用 i 会报错,因为 i 在循环中一直在变。

  • 非阻塞executor.execute()异步的。这意味着主线程会瞬间跑完这个循环,把 20 个任务丢进线程池的任务队列,而不会等待任务执行完。

3、关闭

线程池的工作流程

当你调用 executor.execute() 时,内部会发生以下逻辑:

  1. 核心线程(Core Threads):如果当前运行的线程少于核心线程数,直接创建新线程执行。

  2. 任务队列(Work Queue):如果核心线程满了,任务会进入队列排队。

  3. 最大线程(Max Threads):如果队列也满了,且线程数少于最大线程数,则创建非核心线程。

  4. 拒绝策略:如果全都满了,就会触发拒绝策略(Reject Policy)。


业务场景

订单异步处理

java 复制代码
import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;

public class OrderProcessor {
    
    // 使用单例模式创建线程池
    private static final ThreadPoolExecutor orderExecutor = new ThreadPoolExecutor(
        3, 8, 30, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(1000),
        new ThreadFactory() {
            private int count = 0;
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r, "order-process-" + (++count));
                thread.setDaemon(false);
                return thread;
            }
        },
        new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者线程执行
    );
    
    /**
     * 异步处理订单
     */
    public CompletableFuture<Void> processOrderAsync(Order order) {
        return CompletableFuture.runAsync(() -> {
            try {
                // 1. 验证订单
                validateOrder(order);
                
                // 2. 扣减库存
                reduceInventory(order);
                
                // 3. 生成发货单
                generateShipping(order);
                
                // 4. 发送通知
                sendNotification(order);
                
                System.out.println("订单处理完成: " + order.getId());
            } catch (Exception e) {
                // 记录异常,进行补偿
                handleOrderException(order, e);
            }
        }, orderExecutor);
    }
    
    /**
     * 批量处理订单
     */
    public CompletableFuture<Void> batchProcessOrders(List<Order> orders) {
        List<CompletableFuture<Void>> futures = new ArrayList<>();
        
        for (Order order : orders) {
            CompletableFuture<Void> future = processOrderAsync(order);
            futures.add(future);
        }
        
        // 等待所有任务完成
        return CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0])
        );
    }
    
    // 业务方法(模拟实现)
    private void validateOrder(Order order) {
        // 验证逻辑
    }
    
    private void reduceInventory(Order order) {
        // 扣减库存逻辑
    }
    
    private void generateShipping(Order order) {
        // 生成发货单逻辑
    }
    
    private void sendNotification(Order order) {
        // 发送通知
    }
    
    private void handleOrderException(Order order, Exception e) {
        // 异常处理
    }
    
    // 优雅关闭
    public void shutdown() {
        orderExecutor.shutdown();
    }
    
    // 订单类
    static class Order {
        private String id;
        // 其他字段
        
        public String getId() { return id; }
    }
}
说明

1、为什么返回类型是CompletableFuture<Void>?

答:

其实你要是不想返回的话,直接void就行。线程自己处理业务逻辑,啥都不用管。但是缺点就是,你什么都不知道,无法等待任务完成,而且不知道任务会不会被线程池拒绝。

2、这么写有什么好处呢?

答:

虽然你的逻辑内部不产生结果(即 runAsync 的特性),但返回 CompletableFuture 有以下三个核心好处:

  1. 链式调用: 调用者可以写 processOrderAsync(order).thenRun(() -> System.out.println("全部搞定"))

  2. 异常处理: 调用者可以使用 .exceptionally() 统一处理异步链路中的崩溃。

  3. 等待结束: 在单元测试或系统关闭前,可以调用 .join() 确保任务执行完了。

(关于链式调用的问题,后面会新开一遍文章说一下,爱你。)

3、还有什么常见的返回类型吗?

返回类型 场景建议
CompletableFuture<Void> 推荐。 异步执行,不返回数据,但允许调用者监听状态。
void 极致的"甩手掌柜",调用方完全不关心后续,代码最简。
CompletableFuture<T> 异步执行,且需要把处理后的结果传回给调用方。

异步数据导出

java 复制代码
import java.util.concurrent.*;
import java.util.List;
import java.io.File;

public class DataExportService {
    
    // 专门用于导出任务的线程池
    private static final ThreadPoolExecutor exportExecutor = new ThreadPoolExecutor(
        2, 4, 5, TimeUnit.MINUTES,
        new LinkedBlockingQueue<>(50),
        new ThreadFactory() {
            private int count = 0;
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r, "export-thread-" + (++count));
                thread.setPriority(Thread.NORM_PRIORITY);
                return thread;
            }
        },
        new ThreadPoolExecutor.DiscardOldestPolicy() // 拒绝策略:丢弃最老任务
    );
    
    /**
     * 异步导出Excel
     */
    public CompletableFuture<File> exportExcelAsync(String exportId, 
                                                     List<?> dataList) {
        return CompletableFuture.supplyAsync(() -> {
            System.out.println("开始导出数据,任务ID: " + exportId);
            
            try {
                // 模拟大数据量处理
                File excelFile = generateExcelFile(dataList);
                
                // 模拟上传到云存储
                String url = uploadToCloudStorage(excelFile);
                
                // 记录导出日志
                saveExportLog(exportId, url, "SUCCESS");
                
                return excelFile;
            } catch (Exception e) {
                saveExportLog(exportId, null, "FAILED");
                throw new RuntimeException("导出失败", e);
            }
        }, exportExecutor);
    }
    
    /**
     * 带进度的数据导出
     */
    public CompletableFuture<File> exportWithProgress(String exportId, 
                                                      List<?> dataList,
                                                      ProgressCallback callback) {
        return CompletableFuture.supplyAsync(() -> {
            int total = dataList.size();
            int batchSize = 1000;
            int processed = 0;
            
            for (int i = 0; i < total; i += batchSize) {
                int end = Math.min(i + batchSize, total);
                List<?> batchData = dataList.subList(i, end);
                
                // 处理批次数据
                processBatchData(batchData);
                
                processed = end;
                float progress = (float) processed / total;
                
                // 回调更新进度
                if (callback != null) {
                    callback.onProgress(progress);
                }
                
                // 模拟处理时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            
            return generateExcelFile(dataList);
        }, exportExecutor);
    }
    
    // 业务方法(模拟实现)
    private File generateExcelFile(List<?> dataList) {
        // 生成Excel文件
        return new File("export.xlsx");
    }
    
    private String uploadToCloudStorage(File file) {
        // 上传到云存储
        return "https://oss.example.com/" + file.getName();
    }
    
    private void saveExportLog(String exportId, String url, String status) {
        // 保存日志
    }
    
    private void processBatchData(List<?> batchData) {
        // 处理批次数据
    }
    
    // 进度回调接口
    public interface ProgressCallback {
        void onProgress(float progress);
    }
    
    // 获取线程池状态
    public void printThreadPoolStatus() {
        System.out.println("核心线程数: " + exportExecutor.getCorePoolSize());
        System.out.println("活动线程数: " + exportExecutor.getActiveCount());
        System.out.println("任务队列大小: " + exportExecutor.getQueue().size());
        System.out.println("已完成任务数: " + exportExecutor.getCompletedTaskCount());
    }
}
说明

这里有点看不懂,先说一下吧,exportExcelAsync是最标准的异步流导出,直接调用线程池进行业务逻辑的调用并且返回结果,流程如下:

这是最基础的异步流,采用了 CompletableFuture.supplyAsync

执行步骤:

  1. 提交任务 :将任务交给 exportExecutor 处理。

  2. 生成文件 :调用 generateExcelFile(模拟耗时操作)。

  3. 上传云端:将生成的 File 上传到 OSS 等存储服务。

  4. 保存日志 :无论成功还是失败,都会记录 saveExportLog

  5. 返回结果 :返回一个 File 对象,调用者可以通过 .get().thenAccept() 获取。

而exportWithProgress则是这样子的

这是这段代码的高级之处。它解决了大数据量导出时"用户不知道还要等多久"的问题。

  • 分批处理 (Batching) :它通过 for 循环和 subList 将原始数据切分成每 1000 条一组。

  • 进度计算 :每次处理完一批,计算 processed / total 的百分比。

  • 回调机制 (ProgressCallback) :每完成一个批次,就调用一次 callback.onProgress(progress)

    • 注意: 这个回调通常会连接到 WebSocket 或 Redis,从而让前端页面能实时显示进度条。
  • 模拟延迟Thread.sleep(100) 是为了模拟真实处理数据的耗时,防止瞬时完成看不出进度效果。

两者都用了try catch来保证健壮性

  • 异常处理 :在 try-catch 块中捕获异常,并在失败时记录错误日志,确保即便导出崩了,系统也知道原因。

  • 状态监控 (printThreadPoolStatus):提供了一个监控入口。在实际生产中,我们可以通过这个方法观察队列是否积压,从而判断是否需要增加核心线程数。

可以优化的点:

  • 拒绝策略的风险DiscardOldestPolicy 会让某些用户永远等不到他们的文件(任务被悄悄丢弃了)。在金融或严肃业务中,通常改用 CallerRunsPolicy(让调用者自己执行)或者自定义异常抛出。

  • 内存占用List<?> dataList 如果非常大(比如百万级),直接传入方法可能会导致 OOM (内存溢出)。通常建议传入查询条件,在异步线程里分页从数据库读取。

  • 线程中断exportWithProgress 里的 Thread.sleep 捕获了中断信号并重置了状态,这是非常专业的写法,值得点赞。

也就是这里

// 模拟处理时间

try {

Thread.sleep(100);

} catch (InterruptedException e) {

// 就是这一句!重新设置中断状态

Thread.currentThread().interrupt();

}

为什么说这行代码"很专业"?

在 Java 并发编程中,这是一个非常容易被新手忽略的最佳实践

  1. 中断标志位被"擦除"了

当一个线程正在 sleep 时,如果外部调用了 thread.interrupt()sleep 方法会立刻抛出 InterruptedException重点来了: 一旦抛出这个异常,JVM 会自动把该线程的"中断标志位"清除(改为 false)。

  1. 如果不加这一句会发生什么?

如果你只是打印了日志,或者干脆 catch 块里什么都不写:

  • 线程的中断状态丢失了。

  • 上层代码(或者线程池的后续逻辑)无法知道这个线程曾经被要求停止。

  • 这就像是有人按了"紧急停止"按钮,结果系统捕捉到了信号但转头就给忘了,导致程序继续盲目运行。

  1. Thread.currentThread().interrupt() 的作用

这一行的意思是:"既然异常把中断标志位擦除了,那我就手动把它再设回 true。"

这样做有几个好处:

  • 传递信号: 如果这个任务后续还有其他的检查点(比如 Thread.currentThread().isInterrupted()),它能感知到中断。

  • 尊重规范: 让线程池(exportExecutor)或更高层的调用者能看到线程的中断状态,从而决定是否回收线程或停止后续任务。


定时任务线程池

java 复制代码
import java.util.concurrent.*;
import java.time.LocalDateTime;

public class ScheduledTaskService {
    
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);
    
    /**
     * 初始化定时任务
     */
    public void initScheduledTasks() {
        // 1. 每天凌晨执行数据清理
        scheduleDailyCleanup();
        
        // 2. 每5分钟执行一次数据同步
        schedulePeriodicSync();
        
        // 3. 延迟执行一次性任务
        scheduleOneTimeTask();
    }
    
    /**
     * 每天凌晨2点执行数据清理
     */
    private void scheduleDailyCleanup() {
        long initialDelay = calculateInitialDelay(2, 0); // 凌晨2点
        long period = 24 * 60 * 60; // 24小时
        
        scheduler.scheduleAtFixedRate(() -> {
            try {
                System.out.println("开始数据清理: " + LocalDateTime.now());
                cleanUpOldData();
                System.out.println("数据清理完成: " + LocalDateTime.now());
            } catch (Exception e) {
                System.err.println("数据清理失败: " + e.getMessage());
            }
        }, initialDelay, period, TimeUnit.SECONDS);
    }
    
    /**
     * 每5分钟执行数据同步
     */
    private void schedulePeriodicSync() {
        scheduler.scheduleWithFixedDelay(() -> {
            try {
                syncDataWithExternalSystem();
            } catch (Exception e) {
                // 记录异常,下次继续执行
                System.err.println("数据同步失败: " + e.getMessage());
            }
        }, 0, 5, TimeUnit.MINUTES);
    }
    
    /**
     * 延迟10秒执行一次性任务
     */
    private void scheduleOneTimeTask() {
        scheduler.schedule(() -> {
            System.out.println("执行一次性任务: " + LocalDateTime.now());
        }, 10, TimeUnit.SECONDS);
    }
    
    /**
     * 提交可取消的定时任务
     */
    public ScheduledFuture<?> submitCancellableTask(Runnable task, 
                                                    long initialDelay, 
                                                    long period, 
                                                    TimeUnit unit) {
        return scheduler.scheduleAtFixedRate(task, initialDelay, period, unit);
    }
    
    // 工具方法:计算到指定时间的延迟
    private long calculateInitialDelay(int targetHour, int targetMinute) {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime targetTime = now.withHour(targetHour)
                                     .withMinute(targetMinute)
                                     .withSecond(0);
        
        if (now.isAfter(targetTime)) {
            targetTime = targetTime.plusDays(1);
        }
        
        return java.time.Duration.between(now, targetTime).getSeconds();
    }
    
    // 业务方法
    private void cleanUpOldData() {
        // 清理过期数据
    }
    
    private void syncDataWithExternalSystem() {
        // 同步数据
    }
    
    public void shutdown() {
        scheduler.shutdown();
    }
}
说明

这个之前做过,这里总结一下真正业务中会怎么做

1、Spring的用法

如果项目是 Spring Boot,通常不会手动去 new ScheduledExecutorService。我们会利用 Spring 封装好的注解,配合配置文件。

  • 优点:代码极其简洁,支持 Cron 表达式。

  • 企业级改法 :将时间配置写在 application.yml 或配置中心(Apollo/Nacos)。

java 复制代码
@Component
@Slf4j
public class DataCleanupTask {

    // 从配置文件读取 Cron 表达式,例如:0 0 2 * * ? (每天凌晨2点)
    @Scheduled(cron = "${task.cleanup.cron}")
    public void dailyCleanup() {
        log.info("开始数据清理...");
        try {
            // 业务逻辑
        } catch (Exception e) {
            log.error("清理失败", e);
        }
    }
}

2、分布式锁

代码在单机运行没问题,但现代业务通常是 多实例部署

  • 痛点:如果部署了 3 个节点,凌晨 2 点时,3 个节点会同时跑清理任务,可能导致数据库死锁或重复处理。

  • 方案 :使用 ShedLock 或 Redis 锁,确保同一时间只有一个实例执行。

(当时的统计数据业务就是这么处理的)

java 复制代码
@Scheduled(cron = "0 0 2 * * ?")
@SchedulerLock(name = "dataCleanupTask", lockAtMostFor = "10m", lockAtLeastFor = "1m")
public void scheduledTask() {
    // 只有抢到锁的机器才会执行
}

3、分布式任务调度平台(XXL-JOB / Quartz)

在大型互联网公司,定时任务通常是独立于业务代码 进行管理的。最常用的方案是 XXL-JOB (国内主流)或 Elastic-Job

为什么业务开发喜欢用平台?

  1. 可视化管理:不需要改代码,在网页上就能开关任务、修改执行时间。

  2. 弹性调度:如果一台服务器挂了,平台会自动把任务调度到另一台健康的服务器。

  3. 失败告警:任务失败了会自动发邮件/钉钉通知,还有重试机制。

  4. 执行日志:平台记录了每次执行的耗时、结果,方便排查。

总结

场景 推荐方案
本地小工具/单机脚本 维持你现在的 ScheduledExecutorService (最轻量)
普通 Spring Boot 业务 @Scheduled + 配置文件
多台服务器集群部署 @Scheduled + ShedLock (最简单有效)
中大型分布式系统 XXL-JOBCloud Native CronJob (最专业)

小结

对于线程池的用法做了一点小小的总结,这是个开始。

相关推荐
重生之我要成为代码大佬9 小时前
神经网络基础
人工智能·深度学习·神经网络
cxr82810 小时前
龙虾长程任务测试 —— 撰写零人公司自动化运营实践研究报告
运维·人工智能·自动化·openclaw
key_3_feng10 小时前
PolarDB for AI RAG系统建设方案
人工智能·polardb
mit6.82410 小时前
生成式推荐GR4AD
人工智能
网络工程小王10 小时前
【提示词工程和思维链的讲解】学习笔记
人工智能·笔记·学习
我的Doraemon10 小时前
大模型是怎么被训练出来的?
人工智能·深度学习·机器学习
SomeB1oody10 小时前
【Python深度学习】1.1. 多层感知器MLP(人工神经网络)介绍
开发语言·人工智能·python·深度学习·机器学习
枕石 入梦10 小时前
【源码解析】OpenClaw 多渠道 AI 助手网关的架构设计与核心原理
人工智能·openclaw·小龙虾
财经资讯数据_灵砚智能10 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年4月6日
大数据·人工智能·python·信息可视化·语言模型·自然语言处理·ai编程
逻极10 小时前
Windows平台Ollama AMD GPU编译全攻略:基于ROCm 6.2的实战指南(附构建脚本)
人工智能·windows·gpu·amd·ollama