零基础彻底吃透 Java 线程池:从线程原理到源码实战(万字超详细)
前言
很多 Java 初学者、面试者、初级开发对线程池的认知只停留在 “用来管理线程、避免频繁创建销毁线程” 的浅层概念。
一旦遇到面试高频问题:七大核心参数、四种拒绝策略、五种线程池、工作原理、源码执行流程、为什么不推荐 Executors 创建线程池,就容易卡壳。
本篇文章从零开始,不讲废话、不跳知识点,从「进程与线程基础」→「多线程弊端」→「线程池诞生意义」→「核心参数详解」→「执行源码流程」→「各类线程池对比」→「实战使用规范」层层拆解,零基础也能彻底学懂线程池。
一、前置基础:彻底搞懂进程 & 线程
想要学会线程池,必须先搞懂线程是什么,这是所有知识点的根基。
1.1 什么是进程?
进程是操作系统资源分配的最小单位。
我们电脑上运行的每一个软件都是一个进程:微信、浏览器、IDEA、游戏。
进程拥有独立的内存空间、CPU 资源
进程之间相互隔离、互不干扰
进程创建、销毁开销极大
简单理解:进程就是一个独立运行的程序。
1.2 什么是线程?
线程是 CPU 调度执行的最小单位,也叫轻量级进程。
一个进程包含多个线程,线程共享当前进程的内存资源(堆内存、方法区),各自拥有独立的栈内存。
简单理解:
进程 = 工厂车间
线程 = 车间里的工人
车间资源(原料、机器)所有工人共享
工人干活需要 CPU 调度,工人可以并行工作
1.3 进程和线程的核心区别
资源隔离:进程完全隔离,线程共享进程资源
开销大小:进程创建销毁开销极大,线程开销极小
从属关系:一个进程至少有 1 个线程(主线程)
通信难度:进程间通信复杂,线程间共享变量即可通信
1.4 什么是多线程?
多线程:一个程序中开启多个线程,同时执行多个任务,目的是提高程序执行效率、充分利用 CPU 资源。
Java 程序默认自带主线程 main,我们手写的代码默认都在主线程执行。
二、为什么需要线程池?
学完线程基础,我们思考一个问题:我们直接 new Thread () 创建线程不行吗?为什么要用线程池?
2.1 手动创建线程的弊端
在业务开发中,如果频繁通过 new Thread() 创建线程处理任务,会出现四大致命问题:
1.创建销毁线程开销极高 线程虽然比进程轻量,但频繁创建、销毁线程,会消耗大量 CPU、内存资源,造成资源浪费。
2.无限制创建线程,极易导致 OOM 操作系统的线程数量是有上限的,如果大量任务到来,无限创建线程,会占用全部内存,导致内存溢出 OOM、程序卡死、宕机。
3.线程缺乏统一管理 手动创建的线程杂乱无章,没有统一的分配、调度、监控策略,任务混乱,无法控制并发数量。
4.任务响应速度慢 每次任务到来都需要新建线程,创建线程需要耗时,无法快速响应任务。
2.2 线程池的诞生意义
线程池就是一个线程缓存池,提前创建好若干线程,放入池中统一管理:
线程复用:线程执行完任务不销毁,放回池中,重复利用,避免频繁创建销毁
控制并发数:限制最大线程数,防止无限创建线程导致 OOM
提高响应速度:任务到来直接用空闲线程执行,无需新建
统一管理:统一调度、监控、执行任务,支持延迟、定时任务
一句话总结:线程池就是为了解决手动创建线程的所有弊端,是 Java 并发编程的核心工具。
三、线程池核心类体系
Java 线程池的核心类全部位于 java.util.concurrent 包下,核心体系如下:
3.1 核心继承关系
-
Executor:顶层核心接口,只有一个方法
execute(),定义任务执行规范 -
ExecutorService:继承 Executor,拓展了线程池关闭、任务提交、批量执行等方法
-
ThreadPoolExecutor:真正的线程池实现类(我们工作中手动创建的线程池核心类)
-
ScheduledThreadPoolExecutor:支持定时、延迟任务的线程池
-
Executors:线程池工具类,快速创建各类线程池(阿里规范禁止使用)
重中之重:真正掌控线程池的是 ThreadPoolExecutor,所有线程池底层都是它!
四、线程池七大核心参数
ThreadPoolExecutor 构造方法包含七大核心参数,这是线程池所有原理的核心,下面逐个通俗透彻讲解。
完整构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
4.1 核心参数逐个解析
1. corePoolSize 核心线程数
线程池中长期保留的线程数量,不会被销毁。
- 线程池初始化时没有线程
- 任务不断提交,线程数从 0 开始增加
- 线程数小于核心线程数:优先新建核心线程处理任务
- 核心线程一旦创建,常驻线程池,空闲也不会销毁
通俗理解:正式员工,长期在岗,不裁员。
2. maximumPoolSize 最大线程数
线程池允许创建的最大线程总数。 最大线程数 = 核心线程数 + 非核心线程数
- 当核心线程全部繁忙、任务队列已满,才会创建非核心线程
- 所有线程总数不能超过最大线程数
通俗理解:公司最大在岗人数(正式 + 临时工)。
3. keepAliveTime 空闲存活时间
非核心线程的空闲超时时间。
-
非核心线程空闲时间超过该值,会自动销毁,释放资源
-
核心线程不受该参数影响,永久存活
通俗理解:临时工闲置太久,自动辞退。
4. unit 时间单位
配合 keepAliveTime 的时间单位:毫秒、秒、分钟、小时等。
5. workQueue 任务阻塞队列
存储等待执行任务的队列,核心作用:缓冲任务。
当所有核心线程都在忙碌,新提交的任务不会新建线程,而是存入队列排队。
常用 4 种阻塞队列:
-
ArrayBlockingQueue:有界队列,固定容量,超出容量触发非核心线程创建
-
LinkedBlockingQueue:无界 / 有界队列,默认无界,可无限存任务(极易 OOM)
-
SynchronousQueue:不存储任务的队列,来一个任务必须找线程执行
-
DelayQueue:延迟队列,支持定时任务
6. threadFactory 线程工厂
用来创建线程的工厂,可以自定义:线程名称、线程优先级、是否守护线程。
作用:统一线程创建规则,方便日志排查、线程监控。
7. handler 拒绝策略
当线程池已满(线程数达最大值 + 队列已满),新任务到来的拒绝规则。
五、线程池完整工作原理
结合七大参数,我们梳理任务提交的完整执行流程,这是线程池的灵魂!
5.1 完整执行步骤
-
任务提交,判断当前核心线程数 < corePoolSize → 直接新建核心线程执行任务
-
核心线程已满(核心线程都在干活) → 新任务存入阻塞队列 workQueue排队
-
队列已满,无法存入任务 → 判断当前总线程数 < maximumPoolSize,新建非核心线程执行任务
-
总线程数达到最大值,队列也已满 → 触发拒绝策略,拒绝新任务
-
任务执行完毕 → 核心线程常驻,非核心线程等待
keepAliveTime超时后销毁
5.2 通俗场景模拟
假设配置:核心线程 2,最大线程 5,队列容量 10
-
1-2 个任务:新建 2 个核心线程执行
-
3-12 个任务:核心线程忙碌,10 个任务进队列排队
-
13-15 个任务:队列满,新建 3 个非核心线程执行
-
16 个及以上任务:线程满、队列满,触发拒绝策略
六、四大拒绝策略
线程池满载后,新任务的四种处理策略,JDK 内置 4 种:
1. AbortPolicy(默认)
直接抛出异常 RejectedExecutionException,终止任务,线程池默认策略。 适合:需要及时发现任务失败的业务。
2. CallerRunsPolicy
谁提交的任务,谁自己执行(主线程执行)。 不会抛异常,会降低任务提交速度,削峰填谷。
3. DiscardPolicy
直接丢弃新任务,不抛异常、无日志。 适合:无关紧要、允许丢失的任务。
4. DiscardOldestPolicy
丢弃队列中最旧的未执行任务,执行当前新任务。 适合:时效性高、最新任务优先的业务。
七、JDK 五种内置线程池(原理 + 弊端)
JDK 通过 Executors 工具类提供 5 种常用线程池,但是阿里开发手册明确禁止使用,下面讲清原理和致命弊端。
1. newFixedThreadPool(固定线程池)
Executors.newFixedThreadPool(5);
-
核心线程数 = 最大线程数,无非核心线程
-
队列:无界
LinkedBlockingQueue -
弊端:队列无界,任务堆积无限扩容,导致 OOM
2. newSingleThreadExecutor(单线程池)
Executors.newSingleThreadExecutor();
-
核心、最大线程数都是 1,单线程串行执行
-
队列无界
-
弊端:同样无限堆积任务,OOM 风险
3. newCachedThreadPool(缓存线程池)
Executors.newCachedThreadPool();
-
核心线程 0,最大线程
Integer.MAX_VALUE -
队列:不存储任务的
SynchronousQueue -
弊端:可无限创建线程,瞬间耗尽 CPU 内存,直接 OOM 宕机
4. newScheduledThreadPool(定时线程池)
支持延迟、定时任务,最大线程数 MAX_VALUE,存在无限创建线程风险。
5. newWorkStealingPool(窃取线程池)
JDK1.8 新增,基于 ForkJoinPool,适合并行计算,不常用。
八、线程池核心源码执行流程(极简易懂)
我们看 execute() 方法核心源码,一眼看透底层:
public void execute(Runnable command) {
// 1.任务非空校验
if (command == null)
throw new NullPointerException();
// 获取线程池状态、线程数量
int c = ctl.get();
// 2.线程数小于核心线程数 → 新建核心线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 3.核心线程满,任务入队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
return;
}
// 4.队列满,新建非核心线程,失败则拒绝
if (!addWorker(command, false))
reject(command);
}
九、生产环境线程池实战规范
9.1 手动创建线程池模板
// 自定义线程池(生产标准写法)
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
30, // 空闲时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(100), // 有界队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
9.2 核心参数配置经验
-
CPU 密集型任务(计算多、IO 少) 核心线程数 = CPU 核心数 + 1 充分利用 CPU,避免线程切换开销
-
IO 密集型任务(数据库、网络请求多) 核心线程数 = CPU 核心数 * 2 IO 阻塞多,多开线程提升吞吐量
9.3 线程池使用最佳实践
-
禁止 Executors,全部手动创建 ThreadPoolExecutor
-
必须使用有界队列,限制任务数量
-
自定义线程工厂,设置线程名,方便排查问题
-
根据业务场景选择拒绝策略
-
项目关闭时手动关闭线程池,避免资源泄漏
结语
本篇文章从线程基础 → 线程池痛点 → 七大参数 → 执行原理 → 源码流程 → 实战规范,完整闭环讲解了 Java 线程池所有知识点。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)