【JUC】线程池

Java 线程池是多线程编程中的核心组件,它通过复用线程来减少线程创建和销毁的开销,提高系统性能和资源利用率。下面从核心概念、工作原理、使用方式和最佳实践几个方面来讲解。

一、为什么需要线程池

不用线程池的问题:

  • 每次任务都 new Thread(),创建/销毁开销大
  • 线程数量无法控制,高并发时可能耗尽系统资源(内存、CPU)
  • 缺乏统一的任务管理和监控手段

线程池的优势:

  • 降低资源消耗:复用已创建的线程
  • 提高响应速度:任务到达时无需等待线程创建
  • 提高可管理性:统一分配、调优和监控

二、核心类:ThreadPoolExecutor

Java 线程池的核心实现是 java.util.concurrent.ThreadPoolExecutor,它的构造方法有 7 个关键参数:

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,                  // 核心线程数
    int maximumPoolSize,               // 最大线程数
    long keepAliveTime,                // 空闲线程存活时间
    TimeUnit unit,                     // 时间单位
    BlockingQueue<Runnable> workQueue, // 任务队列
    ThreadFactory threadFactory,       // 线程工厂
    RejectedExecutionHandler handler   // 拒绝策略
)

参数详解

参数 说明
corePoolSize 核心线程数,即使空闲也会保留(除非设置 allowCoreThreadTimeOut)
maximumPoolSize 线程池允许的最大线程数
keepAliveTime 超过核心线程数的空闲线程存活时间
workQueue 存放待执行任务的阻塞队列
threadFactory 创建线程的工厂,可自定义线程名、优先级等
handler 队列满且线程数达最大时的拒绝策略

三、任务提交后的处理流程

这是线程池最核心的机制,理解它非常重要:

复制代码
提交任务
   │
   ├─ 核心线程未满? ──是──> 创建核心线程执行
   │        │否
   ├─ 队列未满? ────是──> 任务入队等待
   │        │否
   ├─ 线程数 < max? ─是──> 创建非核心线程执行
   │        │否
   └─> 执行拒绝策略

关键点:只有当队列满了之后,才会创建超出核心线程数的线程。这个顺序容易被误解。

四、四种拒绝策略

当任务无法被接受时(队列满 + 线程数达 max):

  1. AbortPolicy (默认):抛出 RejectedExecutionException
  2. CallerRunsPolicy:由提交任务的线程自己执行(起降速作用)
  3. DiscardPolicy:直接丢弃,不抛异常
  4. DiscardOldestPolicy:丢弃队列中最老的任务,再尝试提交

也可以自定义实现 RejectedExecutionHandler。

五、Executors 提供的快捷工厂(不推荐)

java 复制代码
Executors.newFixedThreadPool(n);      // 固定线程数,队列无界
Executors.newCachedThreadPool();      // 线程数可无限增长
Executors.newSingleThreadExecutor();  // 单线程,队列无界
Executors.newScheduledThreadPool(n);  // 支持定时/周期任务

《阿里巴巴 Java 开发手册》明确禁止使用 Executors 创建线程池,原因:

  • newFixedThreadPool / newSingleThreadExecutor:使用无界队列 LinkedBlockingQueue,任务堆积可能导致 OOM
  • newCachedThreadPool / newScheduledThreadPool:最大线程数为 Integer.MAX_VALUE,可能创建海量线程导致 OOM

推荐手动 new ThreadPoolExecutor(...),明确各参数。

六、线程池大小如何设置

没有固定公式,取决于任务类型:

  • CPU 密集型:核心数 + 1(减少上下文切换)
  • IO 密集型:核心数 × 2,或更多(线程大部分时间在等待 IO)

更精确的估算:

复制代码
线程数 = 核心数 × 目标CPU利用率 × (1 + 等待时间/计算时间)

实际生产中建议结合压测调整,并配合监控。

七、常用方法

java 复制代码
executor.execute(runnable);       // 提交无返回值任务
Future<T> f = executor.submit(callable); // 提交有返回值任务
executor.shutdown();              // 平滑关闭,不再接受新任务
executor.shutdownNow();           // 立即关闭,尝试中断运行中任务
executor.awaitTermination(...);   // 等待终止
executor.getActiveCount();        // 活跃线程数(监控用)
executor.getQueue().size();       // 队列积压数

八、完整示例

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                              // 核心线程数
    8,                              // 最大线程数
    60L, TimeUnit.SECONDS,          // 空闲存活时间
    new ArrayBlockingQueue<>(100),  // 有界队列
    new ThreadFactory() {           // 自定义线程工厂
        private final AtomicInteger n = new AtomicInteger(1);
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, "my-pool-" + n.getAndIncrement());
            t.setDaemon(false);
            return t;
        }
    },
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

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

executor.shutdown();

九、注意事项与最佳实践

  1. 使用有界队列,避免任务无限堆积导致 OOM
  2. 给线程起有意义的名称,便于排查问题(如 jstack)
  3. 合理设置拒绝策略 ,核心业务建议 CallerRunsPolicy 或自定义
  4. 关闭线程池 :应用停止时调用 shutdown(),避免资源泄漏
  5. 监控线程池:活跃线程数、队列大小、完成任务数、拒绝次数
  6. 避免死锁:父任务等待子任务时,若父子共用同一线程池可能饿死
  7. 区分业务隔离:不同业务用不同线程池,避免相互影响

十、延伸

  • ScheduledThreadPoolExecutor:支持延迟和周期任务
  • ForkJoinPool:分治任务,工作窃取算法(Java 8 parallelStream 底层)
  • CompletableFuture:Java 8 异步编排,可指定线程池
相关推荐
文丑颜不良啊30 分钟前
LeetCode1961~1980题目解析
java·leetcode
文丑颜不良啊1 小时前
LeetCode2001~2020题目解析
java·leetcode
用户3721574261351 小时前
Java 批量提取 Word 文档中的图片并保存到本地
java
SL_staff1 小时前
BI项目失败率90%?JVS如何用三阶闭环构建可落地的数据生产力平台
java·开源·全栈
nice_lcj5203 小时前
ThreadLocal 入门:它是怎么隔离数据的、为什么会导致内存泄漏
java·开发语言·后端·面试·并发编程·内存泄漏·threadlocal
紧跟先前的步伐3 小时前
Go AI Agent 实战 28|Agent 的预算、超时与停止条件
开发语言·人工智能·后端·ai·golang
秋天的怀念_David3 小时前
算法一千零一夜 · 第二十六夜 协议的母语
java·spring boot·算法
灿飞3 小时前
Kotlin与Java虚拟机八:控制流与 when:卫语句、上下文相关解析与穷尽性
java·kotlin
文丑颜不良啊4 小时前
LeetCode2021~2040题目解析
java·leetcode
bksczm4 小时前
InnoDB 行格式详解:DYNAMIC / COMPACT / COMPRESSED / REDUNDANT
java·开发语言·数据库