Java线程池

3 阅读5分钟

Java 里常说的“四种线程池”,通常指 Executors 提供的这 4 种:

  1. newFixedThreadPool
  2. newSingleThreadExecutor
  3. newCachedThreadPool
  4. newScheduledThreadPool

它们本质上都是对 ThreadPoolExecutor 的封装。


一、为什么要用线程池

如果每来一个任务就创建一个线程,会有几个问题:

  • 线程创建和销毁有开销
  • 线程过多会抢占 CPU,导致频繁上下文切换
  • 容易把内存打满
  • 不方便统一管理任务

线程池的作用就是:

  • 复用线程
  • 控制并发数量
  • 管理任务队列
  • 提高系统稳定性和性能

二、线程池核心参数先理解

在看四种线程池前,先看 ThreadPoolExecutor 的几个关键参数:

public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
)

含义如下:

  • corePoolSize:核心线程数,默认会长期保留
  • maximumPoolSize:最大线程数
  • keepAliveTime:非核心线程空闲多久后回收
  • workQueue:任务队列
  • threadFactory:线程工厂,决定线程怎么创建
  • RejectedExecutionHandler:拒绝策略,线程池满了怎么办

三、四种线程池解析


1. FixedThreadPool —— 固定大小线程池

创建方式

ExecutorService executor = Executors.newFixedThreadPool(5);

特点

  • 线程数量固定
  • 多余任务进入阻塞队列等待
  • 适合任务量比较稳定的场景

底层参数本质

new ThreadPoolExecutor(
    nThreads,
    nThreads,
    0L,
    TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>()
)

关键点

  • corePoolSize = maximumPoolSize
  • 使用的是 无界队列 LinkedBlockingQueue

优点

  • 线程数量可控
  • 不会无限创建线程
  • 适合稳定并发任务

缺点

  • 因为队列是无界的,如果任务堆积太多,可能导致:

    • 内存持续增长
    • 最终 OOM

适用场景

  • 服务器处理相对稳定的请求
  • 后台批量处理任务
  • 并发量可预估的业务

2. SingleThreadExecutor —— 单线程线程池

创建方式

ExecutorService executor = Executors.newSingleThreadExecutor();

特点

  • 线程池里只有一个线程
  • 所有任务按顺序串行执行
  • 如果线程挂了,会创建新线程继续执行

底层参数本质

new ThreadPoolExecutor(
    1,
    1,
    0L,
    TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>()
)

优点

  • 保证任务顺序执行
  • 避免多线程并发问题
  • 适合串行化场景

缺点

  • 吞吐量低
  • 一个任务执行太久会阻塞后续所有任务
  • 同样使用无界队列,也有 OOM 风险

适用场景

  • 日志顺序处理
  • 消息顺序消费
  • 需要保证任务串行执行的场景

3. CachedThreadPool —— 可缓存线程池

创建方式

ExecutorService executor = Executors.newCachedThreadPool();

特点

  • 线程数几乎不固定
  • 来一个任务,如果有空闲线程就复用,没有就新建线程
  • 空闲线程 60 秒后回收

底层参数本质

new ThreadPoolExecutor(
    0,
    Integer.MAX_VALUE,
    60L,
    TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>()
)

关键点

  • corePoolSize = 0

  • maximumPoolSize = Integer.MAX_VALUE

  • 队列是 SynchronousQueue

    • 不存储任务
    • 任务必须直接交给线程执行
    • 没有空闲线程就创建新线程

优点

  • 非常适合短时间大量小任务
  • 线程复用能力强
  • 灵活性高

缺点

  • 最大线程数几乎无限

  • 如果任务提交过快,可能疯狂创建线程

  • 导致:

    • CPU 飙升
    • 内存耗尽
    • 系统雪崩

适用场景

  • 轻量、短时、异步的小任务
  • 突发性高并发但任务执行极快的场景

不推荐原因

这是面试里高频点:

newCachedThreadPool() 容易因为线程数量无限增长导致系统风险。


4. ScheduledThreadPool —— 定时线程池

创建方式

ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);

作用

支持:

  • 延迟执行任务
  • 周期性执行任务

示例

延迟执行
executor.schedule(() -> {
    System.out.println("3秒后执行");
}, 3, TimeUnit.SECONDS);
周期执行
executor.scheduleAtFixedRate(() -> {
    System.out.println("每2秒执行一次");
}, 1, 2, TimeUnit.SECONDS);

底层本质

底层是 ScheduledThreadPoolExecutor,核心是一个延迟队列。

优点

  • 支持定时任务
  • Timer 更强大
  • 多线程执行,不会因为一个任务异常导致全部任务停止

缺点

  • 如果周期任务执行时间过长,可能影响调度精度
  • 如果线程数设置不合理,任务会堆积

适用场景

  • 定时同步数据
  • 周期性清理缓存
  • 延迟任务执行
  • 轮询调度任务

四、四种线程池对比

线程池线程数量队列类型特点风险
FixedThreadPool固定LinkedBlockingQueue(无界)并发数固定任务堆积可能 OOM
SingleThreadExecutor1 个LinkedBlockingQueue(无界)串行执行堆积可能 OOM,吞吐低
CachedThreadPool不固定,几乎无限SynchronousQueue灵活,适合短任务线程暴涨
ScheduledThreadPool核心线程固定DelayedWorkQueue定时/周期任务任务堆积、调度延迟

五、面试高频点:为什么不建议直接用 Executors 创建线程池

这是非常重要的点。

阿里巴巴 Java 开发手册里建议:

不允许使用 Executors 去创建线程池,而是通过 ThreadPoolExecutor 手动创建。

原因:

1. FixedThreadPoolSingleThreadExecutor

使用无界队列:

LinkedBlockingQueue

任务过多会无限堆积,可能导致 OOM。

2. CachedThreadPool

最大线程数:

Integer.MAX_VALUE

可能创建大量线程,导致系统资源耗尽。

3. ScheduledThreadPool

也可能因为任务堆积导致内存压力过大。


六、实际开发推荐方式

建议手动创建线程池,显式指定参数。

示例:自定义线程池

import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                4,                         // 核心线程数
                8,                         // 最大线程数
                60,                        // 空闲存活时间
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100), // 有界队列
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        for (int i = 0; i < 10; i++) {
            int num = i;
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行任务:" + num);
            });
        }

        executor.shutdown();
    }
}

七、常见拒绝策略

当线程池满了,任务放不进去时,会触发拒绝策略。

1. AbortPolicy

直接抛异常,默认策略

new ThreadPoolExecutor.AbortPolicy()

2. CallerRunsPolicy

由提交任务的线程自己执行

new ThreadPoolExecutor.CallerRunsPolicy()

3. DiscardPolicy

直接丢弃任务,不报错

new ThreadPoolExecutor.DiscardPolicy()

4. DiscardOldestPolicy

丢弃队列中最老的任务,再尝试提交当前任务

new ThreadPoolExecutor.DiscardOldestPolicy()

八、线程池执行流程

线程池接收任务时,一般按这个顺序处理:

  1. 如果当前线程数 < 核心线程数,创建核心线程执行
  2. 否则尝试把任务放入队列
  3. 如果队列满了,且线程数 < 最大线程数,创建非核心线程执行
  4. 如果线程数也到上限了,就执行拒绝策略

这个流程是面试常考。