java线程池详解

线程池
一、线程池的核心概念
线程池本质上是一个管理线程的容器,它预先创建一定数量的线程,复用这些线程来执行多个任务,避免了频繁创建和销毁线程带来的性能开销。
1. 为什么要用线程池?

  • 降低资源消耗:复用已创建的线程,减少线程创建 / 销毁的系统开销。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 便于管理:统一控制线程的数量、优先级、任务队列等,避免无限创建线程导致 OOM(内存溢出)。

2. 线程池的核心参数(ThreadPoolExecutor)
Java 中线程池的核心实现是java.util.concurrent.ThreadPoolExecutor,其构造方法的核心参数如下:

public ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数(常驻线程数)
    int maximumPoolSize,     // 最大线程数(线程池能容纳的最大线程数)
    long keepAliveTime,      // 非核心线程的空闲存活时间
    TimeUnit unit,           // keepAliveTime的时间单位
    BlockingQueue<Runnable> workQueue,  // 任务等待队列
    ThreadFactory threadFactory,        // 线程创建工厂(自定义线程名称、优先级等)
    RejectedExecutionHandler handler    // 拒绝策略(任务过多时的处理方式)
)

参数详解:

  • 核心线程数:线程池长期保持的线程数量,即使线程空闲也不会销毁(除非设置allowCoreThreadTimeOut=true)。

  • 最大线程数:线程池允许创建的最大线程数,核心线程满了 + 队列满了才会创建非核心线程。

  • 空闲存活时间:非核心线程空闲超过该时间会被销毁,释放资源。

  • 任务队列:核心线程都在忙时,新任务会进入队列等待,常见的队列类型:

    • ArrayBlockingQueue:有界数组队列(推荐,避免队列无限扩容)。
    • LinkedBlockingQueue:无界链表队列(默认容量Integer.MAX_VALUE,易导致 OOM)。
    • SynchronousQueue:直接传递队列(不存储任务,直接交给线程执行)。
  • 拒绝策略:当线程数达到最大值 + 队列满时,新任务的处理方式:

    • AbortPolicy(默认):直接抛出RejectedExecutionException。
    • CallerRunsPolicy:由提交任务的线程自己执行。
    • DiscardPolicy:直接丢弃新任务,无任何提示。
    • DiscardOldestPolicy:丢弃队列中最旧的任务,尝试加入新任务。

二、线程池的工作流程

image

三、常用的线程池(Executors 工具类)
Java 提供了Executors工具类快速创建线程池,但生产环境不推荐直接使用(多数是无界队列,易导致 OOM),以下是常见类型:

线程池类型 核心参数 适用场景
FixedThreadPool 核心线程数 = 最大线程数,无界队列 执行长期稳定的任务(如后台任务)
SingleThreadExecutor 核心线程数 = 最大线程数 = 1,无界队列 任务需串行执行的场景
CachedThreadPool 核心线程数 = 0,最大线程数 = Integer.MAX_VALUE 短期、高频的异步任务(如临时请求)
ScheduledThreadPool 核心线程数固定,支持定时 / 延迟任务 定时任务(如定时清理缓存)

示例:自定义线程池(推荐)

import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 1. 定义核心参数
        int corePoolSize = 5;          // 核心线程数
        int maximumPoolSize = 10;      // 最大线程数
        long keepAliveTime = 60L;      // 非核心线程空闲60秒销毁
        TimeUnit unit = TimeUnit.SECONDS;
        // 有界任务队列(容量100)
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
        // 自定义线程工厂(设置线程名称,便于排查问题)
        ThreadFactory threadFactory = new ThreadFactory() {
            private int count = 1;
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("custom-thread-" + count++);
                return thread;
            }
        };
        // 拒绝策略:提交任务的线程自己执行
        RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();

        // 2. 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime, unit,
                workQueue, threadFactory, handler
        );

        // 3. 提交任务
        for (int i = 0; i < 20; i++) {
            int taskNum = i;
            executor.execute(() -> {
                System.out.println("任务" + taskNum + "由线程" + Thread.currentThread().getName() + "执行");
                try {
                    Thread.sleep(1000); // 模拟任务执行
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 4. 关闭线程池(重要!)
        executor.shutdown(); // 平缓关闭:等待已提交的任务执行完
        // executor.shutdownNow(); // 强制关闭:中断所有正在执行的任务,返回未执行的任务列表
    }
}

说明:

  • execute():提交无返回值的任务(Runnable)。
  • submit():提交有返回值的任务(Callable),返回Future对象,可获取任务结果。
  • submit():提交有返回值的任务(Callable),返回Future对象,可获取任务结果。

四、线程池的最佳实践
1、避免使用 Executors 默认创建:手动创建ThreadPoolExecutor,指定有界队列和合理的拒绝策略。
2、核心线程数设置

  • CPU 密集型任务(如计算):核心线程数 = CPU 核心数 + 1。
  • IO 密集型任务(如网络请求、文件读写):核心线程数 = CPU 核心数 * 2。

3、监控线程池状态:通过ThreadPoolExecutor的方法监控(getActiveCount()、getQueueSize()、getCompletedTaskCount())。
4、处理任务异常

  • 任务内捕获所有异常,避免线程终止。
  • 使用submit()提交任务,通过Future.get()捕获异常。

线程池优化
一、核心参数的精细化配置(最基础也最关键)
线程池的性能瓶颈往往源于参数配置不合理,优化的核心是让核心线程数、最大线程数、任务队列 与业务场景匹配。

  1. 核心线程数(corePoolSize)优化
    核心线程数决定了线程池的基础处理能力,需根据任务类型精准设置:
  • CPU 密集型任务(如数据计算、排序)
    这类任务 CPU 利用率高,线程数过多会导致上下文切换开销增大。
    推荐值:CPU核心数 + 1(+1 是为了防止某个线程因缺页中断等原因阻塞时,还有线程能利用 CPU)。
    可通过Runtime.getRuntime().availableProcessors()获取 CPU 核心数。
  • IO 密集型任务(如数据库查询、网络请求、文件读写)
    这类任务线程大部分时间在等待 IO,CPU 利用率低,可配置更多线程。
    推荐值:CPU核心数 * 2 或 CPU核心数 / (1 - 阻塞系数)(阻塞系数通常取 0.8~0.9)。
  • 混合任务
    拆分 CPU 密集和 IO 密集任务到不同线程池,避免互相干扰。
  1. 任务队列(workQueue)优化
    核心原则:必须使用有界队列,杜绝无界队列(如默认的 LinkedBlockingQueue)导致的 OOM。
  • 队列类型选择:

    • 任务执行时间短、数量多:用ArrayBlockingQueue(有界数组队列,内存占用固定,存取效率高)。
    • 任务执行时间长、优先级有差异:用PriorityBlockingQueue(优先级队列,确保重要任务先执行)。
    • 需直接传递任务(无队列缓冲):用SynchronousQueue(配合 CachedThreadPool 风格的线程池)。
  • 队列容量设置:
    队列容量 = (峰值任务数 - 核心线程数 * 每秒处理任务数)* 平均任务执行时间
    避免队列过大(任务等待时间长)或过小(频繁创建非核心线程,增加开销)。

  1. 最大线程数(maximumPoolSize)与空闲时间(keepAliveTime)优化
  • 最大线程数:核心线程数 + 队列容量无法处理峰值任务时的补充,建议不超过核心线程数的 2~3 倍(IO 密集型可适当提高),避免线程过多导致上下文切换。
  • 空闲时间:非核心线程的存活时间,IO 密集型任务可设短(如 30 秒),CPU 密集型可设长(如 60 秒),核心线程可通过allowCoreThreadTimeOut(true)允许超时销毁(适用于任务量波动大的场景)。

4. 拒绝策略(RejectedExecutionHandler)优化
默认的AbortPolicy直接抛异常,生产环境需根据业务场景自定义或选择更合理的策略:

  • 核心业务任务:用CallerRunsPolicy(提交任务的线程执行,降低任务丢失风险),或自定义拒绝策略(如记录日志 + 任务入消息队列重试)。
  • 非核心业务任务:用DiscardOldestPolicy(丢弃最旧任务)或DiscardPolicy(丢弃新任务),但需记录丢弃日志。

自定义拒绝策略示例:

RejectedExecutionHandler customHandler = (r, executor) -> {
    // 1. 记录任务丢弃日志(关键)
    System.err.println("任务" + r.toString() + "被拒绝,当前线程池活跃数:" + executor.getActiveCount());
    // 2. 核心任务可尝试重试,或写入MQ/数据库兜底
    if (!executor.isShutdown()) {
        try {
            // 尝试将任务放入队列(阻塞1秒)
            executor.getQueue().offer(r, 1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
};

二、线程池的监控与调优
1. 核心监控指标
通过ThreadPoolExecutor的内置方法获取:

  • getActiveCount():当前活跃线程数(正在执行任务的线程)。
  • getQueue().size():队列中等待的任务数。
  • getCompletedTaskCount():已完成的任务总数。
  • getLargestPoolSize():线程池曾达到的最大线程数(判断是否达到最大线程数)。
  • getRejectedExecutionCount():被拒绝的任务数(拒绝策略是否触发)。

2. 监控实现方式

  • 简单监控:定时打印指标(如每 10 秒)。
  • 集成监控系统:将指标上报到 Prometheus、Micrometer 等,结合 Grafana 可视化。

监控代码示例:

ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
// 每10秒监控一次
monitor.scheduleAtFixedRate(() -> {
    ThreadPoolExecutor executor = (ThreadPoolExecutor) threadPool;
    System.out.println("===== 线程池监控 =====");
    System.out.println("活跃线程数:" + executor.getActiveCount());
    System.out.println("等待任务数:" + executor.getQueue().size());
    System.out.println("已完成任务数:" + executor.getCompletedTaskCount());
    System.out.println("拒绝任务数:" + executor.getRejectedExecutionCount());
    System.out.println("======================");
}, 0, 10, TimeUnit.SECONDS);

3. 动态调优
基于监控数据实时调整参数(线程池支持运行时修改核心参数):

// 运行时修改核心线程数
executor.setCorePoolSize(8);
// 运行时修改最大线程数
executor.setMaximumPoolSize(15);
// 运行时修改非核心线程空闲时间
executor.setKeepAliveTime(30, TimeUnit.SECONDS);

场景:高峰时段(如秒杀)临时提高最大线程数,低峰时段降低,减少资源占用。
三、使用层面的优化
1. 避免线程池滥用

  • 按业务维度拆分线程池:如 “订单处理线程池”、“日志处理线程池”,避免一个线程池处理所有任务(核心任务被非核心任务阻塞)。
  • 避免创建临时线程池:每次创建新线程池会导致线程频繁创建销毁,应复用全局线程池。

2. 任务层面优化

  • 任务粒度适中:任务太小(如仅执行一行代码)会增加线程切换开销;任务太大(如执行 10 分钟的批处理)会导致线程长时间占用,降低并发度。
  • 捕获任务内异常:线程池的线程若因未捕获异常终止,会导致线程池创建新线程,增加开销。
executor.execute(() -> {
    try {
        // 任务逻辑
    } catch (Exception e) {
        // 记录异常,避免线程终止
        log.error("任务执行异常", e);
    }
}); 
  • 避免任务阻塞:IO 任务尽量使用异步 API(如 CompletableFuture),减少线程阻塞时间。

3. 线程池关闭优化

  • 平缓关闭:使用shutdown()而非shutdownNow(),等待已提交的任务执行完毕。
  • 优雅退出:关闭前等待任务执行完成,避免强制终止导致数据丢失。
// 关闭线程池
executor.shutdown();
// 等待60秒,确认所有任务执行完毕
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
    // 超时后强制关闭
    executor.shutdownNow();
}

四、高级优化:自定义线程池扩展
通过继承ThreadPoolExecutor重写钩子方法,实现更精细化的控制:

public class CustomThreadPool extends ThreadPoolExecutor {
    // 任务执行前的钩子方法
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        // 记录任务开始时间、线程ID等
        log.info("任务{}开始执行,线程:{}", r.toString(), t.getName());
    }

    // 任务执行后的钩子方法
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        // 记录任务结束时间、执行耗时,捕获异常
        if (t != null) {
            log.error("任务{}执行异常", r.toString(), t);
        } else {
            log.info("任务{}执行完成", r.toString());
        }
    }

    // 线程池关闭时的钩子方法
    @Override
    protected void terminated() {
        super.terminated();
        log.info("线程池已关闭");
    }
}

总结

  • 参数优化:核心是根据任务类型(CPU/IO 密集)精准配置核心线程数、最大线程数,使用有界队列和业务适配的拒绝策略。
  • 监控调优:通过监控活跃线程数、队列长度、拒绝任务数等指标,动态调整线程池参数,避免性能瓶颈。
  • 使用优化:按业务拆分线程池、捕获任务异常、避免线程阻塞,确保线程池稳定高效运行。
posted @ 2026-01-13 10:30  daring_ding  阅读(4)  评论(0)    收藏  举报