并发01:线程模型
从 ThreadPoolExecutor 的执行流程到 ForkJoinPool 的分治策略,再到线程状态的流转机制,拆解 Java 并发编程的线程模型基础。
线程模型解决什么问题
Java 并发编程的核心问题是:如何在有限的 CPU 资源下,高效地处理大量并发任务。线程模型就是这个问题的解法框架——它定义了任务如何分配、线程如何管理、以及资源如何协调。
最常见的线程模型是「生产者-消费者」模式:任务作为生产的产物,被提交到队列中;工作线程作为消费者,从队列中获取任务并执行。ThreadPoolExecutor 就是这种模型的标准实现。
ThreadPoolExecutor 核心结构
一个 ThreadPoolExecutor 主要由以下几个部分组成:
| 组件 | 作用 | 默认值 |
|---|---|---|
corePoolSize | 核心线程数,常驻工作线程 | CPU 核心数 |
maximumPoolSize | 最大线程数,线程池上限 | CPU 核心数 * 2 |
workQueue | 任务队列,存放等待执行的任务 | LinkedBlockingQueue(无界) |
keepAliveTime | 非核心线程空闲存活时间 | 60s |
threadFactory | 线程工厂,创建线程 | Executors.defaultThreadFactory() |
handler | 拒绝策略,处理无法提交的任务 | AbortPolicy(抛出异常) |
任务提交流程
流程的关键路径是:先看线程数 → 再看队列 → 最后才是拒绝策略。最容易误解的是 maximumPoolSize 的生效条件——只有当队列满了之后,非核心线程才会被创建。如果使用无界队列,任务会一直进入队列,maximumPoolSize 实际上不会生效。
队列类型对线程池行为的影响
| 队列类型 | 特点 | 适用场景 |
|---|---|---|
ArrayBlockingQueue | 有界队列,容量固定 | 需要明确系统承载能力 |
LinkedBlockingQueue | 默认无界,可指定容量 | 任务量不可预测,但需要内存保护 |
SynchronousQueue | 不存任务,直接交接 | 任务必须立即执行,不能排队 |
PriorityBlockingQueue | 按优先级排序 | 需要任务优先级控制 |
线程状态流转
Java 线程有 6 种状态,理解它们的流转关系是排查并发问题的基础:
NEW -> RUNNABLE -> BLOCKED -> WAITING -> TIMED_WAITING -> TERMINATED
| 状态 | 含义 | 触发方式 |
|---|---|---|
NEW | 线程已创建,未启动 | new Thread() |
RUNNABLE | 可运行状态 | start()、获得 CPU 时间片 |
BLOCKED | 阻塞状态,等待锁 | synchronized、ReentrantLock.lock() |
WAITING | 等待状态,无超时 | wait()、join()、LockSupport.park() |
TIMED_WAITING | 等待状态,有超时 | sleep()、wait(timeout)、join(timeout) |
TERMINATED | 终止状态 | 线程执行完毕或异常退出 |
线程池中的工作线程在执行任务时,会在 RUNNABLE、WAITING(等待任务)和 BLOCKED(等待锁)之间切换。
ForkJoinPool 分治模型
ForkJoinPool 是 Java 7 引入的另一种线程模型,专门用于处理可以递归分解的任务。它基于「分治」思想:
大任务 -> fork() 拆分成小任务
-> 多个线程并行执行
-> join() 合并结果
ForkJoinPool 的核心特点
- 工作窃取(Work Stealing):空闲线程会从其他线程的任务队列末尾窃取任务,减少线程空闲时间
- 双端队列:每个线程维护一个双端队列(Deque),自己的任务从头部取,窃取的任务从尾部取
- 递归友好:适合
map-reduce风格的计算任务
使用场景对比
| ThreadPoolExecutor | ForkJoinPool |
|---|---|
| 适合独立、无依赖的任务 | 适合可递归分解的任务 |
| 任务之间没有依赖关系 | 子任务之间有明确的组合关系 |
| CPU 密集型和 IO 密集型都适用 | 更适合 CPU 密集型计算 |
线程池配置原则
配置线程池不是简单地设几个数字,而是要根据任务类型和业务目标来决定:
CPU 密集型任务
这类任务主要消耗 CPU 资源,线程数过多反而会增加上下文切换开销。
int coreSize = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
coreSize,
coreSize,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>()
);
IO 密集型任务
这类任务大部分时间在等待 IO(数据库、网络调用等),需要更多线程来利用等待时间。
int coreSize = Runtime.getRuntime().availableProcessors() * 2;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
coreSize,
coreSize * 2,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000)
);
关键配置原则
- 线程池命名:使用自定义
ThreadFactory给线程命名,方便排查问题 - 队列有界:避免无界队列导致内存溢出
- 拒绝策略:根据业务场景选择合适的拒绝策略
- 资源隔离:不同类型的任务使用不同的线程池,避免相互影响
常见误区
误区一:线程数越多越好
线程数超过 CPU 核心数后,性能提升会边际递减,甚至因为上下文切换而下降。
误区二:异步化就能解决性能问题
异步只是把等待从当前线程转移到工作线程,如果下游慢,问题只是换了个地方出现。
误区三:使用 Executors 静态方法创建线程池
Executors.newFixedThreadPool()、Executors.newCachedThreadPool() 等方法创建的线程池有潜在问题:
newFixedThreadPool使用无界队列newCachedThreadPool线程数没有上限
建议手动创建 ThreadPoolExecutor,显式控制每个参数。
项目中的判断方法
当你在项目中遇到并发问题时,可以按以下步骤判断:
- 任务类型是什么:CPU 密集还是 IO 密集
- 当前线程池配置是否合理:核心线程数、队列容量、拒绝策略
- 线程状态是什么:通过
jstack或监控工具查看线程卡在哪里 - 是否需要资源隔离:不同优先级的任务是否应该分开处理
理解线程模型是掌握 Java 并发编程的第一步。从 ThreadPoolExecutor 的执行流程到 ForkJoinPool 的分治策略,再到线程状态的流转,这些知识构成了并发编程的基础框架。