深入浅出Java线程池

158 阅读5分钟

深入浅出Java线程池

什么是线程池?

想象你开了一家快递站,每天有很多包裹要派送。如果每来一个包裹就雇一个新快递员,送完就解雇,这样效率很低,因为:

  1. 频繁招聘和解雇成本高(对应线程创建和销毁开销大)
  2. 新快递员不熟悉路线(线程需要时间初始化)
  3. 快递员太多时管理混乱(系统资源耗尽)

线程池就像你预先雇佣的一批固定快递员(线程),有包裹(任务)来了就分配给他们,送完继续等待新任务,这样效率更高。

为什么需要线程池?

  1. 降低资源消耗:重复利用已创建的线程,避免频繁创建销毁
  2. 提高响应速度:任务到达时直接使用现有线程,无需等待线程创建
  3. 便于管理:可以统一分配、监控和调优线程资源

Java线程池核心类

Java中的线程池主要通过java.util.concurrent包中的ExecutorService接口及其实现类ThreadPoolExecutor来实现。

线程池工作原理

线程池就像一个有管理的"线程工厂+任务队列":

  1. 核心线程:池中常驻的基本劳动力,即使空闲也不销毁
  2. 任务队列:当核心线程都忙时,新任务进入队列等待
  3. 非核心线程:当队列满了,创建额外线程帮忙(有数量限制)
  4. 拒绝策略:当线程和队列都满了,如何处理新任务

创建线程池的常用方法

Java提供了Executors工厂类来创建常见类型的线程池:

// 1. 固定大小线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(5);

// 2. 单线程池(保证任务顺序执行)
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();

// 3. 可缓存线程池(自动扩容缩容)
ExecutorService cachedPool = Executors.newCachedThreadPool();

// 4. 定时任务线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);

更灵活的ThreadPoolExecutor

实际上,上述工厂方法内部都是使用ThreadPoolExecutor构造的。直接使用它可更精细控制:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,                      // 核心线程数
    10,                     // 最大线程数
    60,                     // 空闲线程存活时间(秒)
    TimeUnit.SECONDS,       // 时间单位
    new ArrayBlockingQueue<>(100), // 任务队列
    Executors.defaultThreadFactory(), // 线程工厂
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

线程池重要参数

  1. corePoolSize:核心线程数,池中常驻线程数量
  2. maximumPoolSize:最大线程数,池中允许的最大线程数
  3. keepAliveTime:非核心线程空闲时的存活时间
  4. workQueue:任务队列,保存等待执行的任务
  5. threadFactory:创建线程的工厂
  6. handler:拒绝策略,当线程和队列都满时的处理方式

四种拒绝策略

  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常
  2. CallerRunsPolicy:让提交任务的线程自己执行该任务
  3. DiscardPolicy:默默丢弃无法处理的任务
  4. DiscardOldestPolicy:丢弃队列中最老的任务,然后重试提交

线程池生命周期

  1. RUNNING:接受新任务,处理队列任务
  2. SHUTDOWN:不接受新任务,但处理队列中的任务
  3. STOP:不接受新任务,不处理队列任务,中断正在执行的任务
  4. TIDYING:所有任务终止,workerCount为0
  5. TERMINATED:terminated()方法执行完毕

使用示例

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(3);
        
        // 提交10个任务
        for (int i = 1; i <= 10; i++) {
            final int taskId = i;
            pool.execute(() -> {
                System.out.println("任务" + taskId + "正在执行,线程:" + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // 模拟任务耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("任务" + taskId + "执行完毕");
            });
        }
        
        // 关闭线程池
        pool.shutdown();
    }
}

任务提交后的完整流程(逐层判断)

流程图解:
graph TD
    A[提交新任务] --> B{核心线程是否空闲?}
    B -->|是| C[交给空闲核心线程执行]
    B -->|否| D{任务队列是否未满?}
    D -->|是| E[任务入队等待]
    D -->|否| F{当前线程数 < maximumPoolSize?}
    F -->|是| G[创建非核心线程执行]
    F -->|否| H[触发拒绝策略]
分步骤详解:
  1. 核心线程优先

    • 如果当前运行的线程数 < corePoolSize(默认4个),立即创建新线程执行任务(即使其他核心线程空闲)。
    • 注:可通过allowCoreThreadTimeOut(true)让核心线程超时回收。
  2. 任务队列缓冲

    • 如果核心线程全忙,任务会被放入workQueue(如LinkedBlockingQueue)。
    • 经典坑点:如果使用无界队列(如未设置容量的LinkedBlockingQueue),maximumPoolSize参数会失效,永远不会创建非核心线程。
  3. 非核心线程应急

    • 当队列已满且线程数 < maximumPoolSize(如8个),会创建临时线程处理新任务。
    • 这些线程在空闲keepAliveTime后会被回收(默认只回收非核心线程)。
  4. 拒绝策略兜底

    • 当队列满且线程数达到maximumPoolSize时,触发RejectedExecutionHandler
    • 内置4种策略:
      • AbortPolicy(默认):直接抛出RejectedExecutionException
      • CallerRunsPolicy:让提交任务的线程自己执行
      • DiscardPolicy:静默丢弃任务
      • DiscardOldestPolicy:丢弃队列中最旧的任务后重试

关键参数组合的实战表现

场景1:核心线程4 + 最大线程8 + 有界队列(容量10)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4, 8, 30, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(10) // 有界队列
);
  • 任务提交顺序
    1. 前4个任务 → 立即创建4个核心线程执行
    2. 第5~14个任务 → 进入队列
    3. 第15~18个任务 → 创建4个非核心线程(总线程数=8)
    4. 第19个任务 → 触发拒绝策略
场景2:核心线程4 + 最大线程4 + 无界队列
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4, 4, 0, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>() // 无界队列
);
  • 表现
    • 永远只有4个核心线程,新任务无限排队(maximumPoolSize无效)
    • 风险:可能引发OOM(队列无限增长)

如何监控和调优?

关键监控API:
executor.getPoolSize();       // 当前线程数
executor.getActiveCount();    // 正在执行任务的线程数
executor.getQueue().size();   // 队列中等待的任务数
executor.getCompletedTaskCount(); // 已完成任务数
调优建议:
  1. CPU密集型任务

    • 推荐线程数 = CPU核数 + 1
    • 使用有界队列防止资源耗尽
  2. IO密集型任务

    • 推荐线程数 = CPU核数 * (1 + 平均等待时间/平均计算时间)
    • 示例:8核CPU,任务50%时间在IO → 约8*(1+1)=16个线程
  3. 超时控制

    // 让核心线程也能超时回收
    executor.allowCoreThreadTimeOut(true);