并发01:线程模型

从 ThreadPoolExecutor 的执行流程到 ForkJoinPool 的分治策略,再到线程状态的流转机制,拆解 Java 并发编程的线程模型基础。

字数 1294 阅读时长 ≈ 4 分钟 2026-5-25 2026-7-12
并发01:线程模型

线程模型解决什么问题

Java 并发编程的核心问题是:如何在有限的 CPU 资源下,高效地处理大量并发任务。线程模型就是这个问题的解法框架——它定义了任务如何分配、线程如何管理、以及资源如何协调。

最常见的线程模型是「生产者-消费者」模式:任务作为生产的产物,被提交到队列中;工作线程作为消费者,从队列中获取任务并执行。ThreadPoolExecutor 就是这种模型的标准实现。

ThreadPoolExecutor 核心结构

一个 ThreadPoolExecutor 主要由以下几个部分组成:

组件作用默认值
corePoolSize核心线程数,常驻工作线程CPU 核心数
maximumPoolSize最大线程数,线程池上限CPU 核心数 * 2
workQueue任务队列,存放等待执行的任务LinkedBlockingQueue(无界)
keepAliveTime非核心线程空闲存活时间60s
threadFactory线程工厂,创建线程Executors.defaultThreadFactory()
handler拒绝策略,处理无法提交的任务AbortPolicy(抛出异常)

任务提交流程

ThreadPoolExecutor 提交流程

流程的关键路径是:先看线程数 → 再看队列 → 最后才是拒绝策略。最容易误解的是 maximumPoolSize 的生效条件——只有当队列满了之后,非核心线程才会被创建。如果使用无界队列,任务会一直进入队列,maximumPoolSize 实际上不会生效。

队列类型对线程池行为的影响

队列类型特点适用场景
ArrayBlockingQueue有界队列,容量固定需要明确系统承载能力
LinkedBlockingQueue默认无界,可指定容量任务量不可预测,但需要内存保护
SynchronousQueue不存任务,直接交接任务必须立即执行,不能排队
PriorityBlockingQueue按优先级排序需要任务优先级控制

线程状态流转

Java 线程有 6 种状态,理解它们的流转关系是排查并发问题的基础:

NEW -> RUNNABLE -> BLOCKED -> WAITING -> TIMED_WAITING -> TERMINATED
状态含义触发方式
NEW线程已创建,未启动new Thread()
RUNNABLE可运行状态start()、获得 CPU 时间片
BLOCKED阻塞状态,等待锁synchronizedReentrantLock.lock()
WAITING等待状态,无超时wait()join()LockSupport.park()
TIMED_WAITING等待状态,有超时sleep()wait(timeout)join(timeout)
TERMINATED终止状态线程执行完毕或异常退出

线程池中的工作线程在执行任务时,会在 RUNNABLEWAITING(等待任务)和 BLOCKED(等待锁)之间切换。

ForkJoinPool 分治模型

ForkJoinPool 是 Java 7 引入的另一种线程模型,专门用于处理可以递归分解的任务。它基于「分治」思想:

大任务 -> fork() 拆分成小任务
         -> 多个线程并行执行
         -> join() 合并结果

ForkJoinPool 的核心特点

  1. 工作窃取(Work Stealing):空闲线程会从其他线程的任务队列末尾窃取任务,减少线程空闲时间
  2. 双端队列:每个线程维护一个双端队列(Deque),自己的任务从头部取,窃取的任务从尾部取
  3. 递归友好:适合 map-reduce 风格的计算任务

使用场景对比

ThreadPoolExecutorForkJoinPool
适合独立、无依赖的任务适合可递归分解的任务
任务之间没有依赖关系子任务之间有明确的组合关系
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)
);

关键配置原则

  1. 线程池命名:使用自定义 ThreadFactory 给线程命名,方便排查问题
  2. 队列有界:避免无界队列导致内存溢出
  3. 拒绝策略:根据业务场景选择合适的拒绝策略
  4. 资源隔离:不同类型的任务使用不同的线程池,避免相互影响

常见误区

误区一:线程数越多越好

线程数超过 CPU 核心数后,性能提升会边际递减,甚至因为上下文切换而下降。

误区二:异步化就能解决性能问题

异步只是把等待从当前线程转移到工作线程,如果下游慢,问题只是换了个地方出现。

误区三:使用 Executors 静态方法创建线程池

Executors.newFixedThreadPool()Executors.newCachedThreadPool() 等方法创建的线程池有潜在问题:

  • newFixedThreadPool 使用无界队列
  • newCachedThreadPool 线程数没有上限

建议手动创建 ThreadPoolExecutor,显式控制每个参数。

项目中的判断方法

当你在项目中遇到并发问题时,可以按以下步骤判断:

  1. 任务类型是什么:CPU 密集还是 IO 密集
  2. 当前线程池配置是否合理:核心线程数、队列容量、拒绝策略
  3. 线程状态是什么:通过 jstack 或监控工具查看线程卡在哪里
  4. 是否需要资源隔离:不同优先级的任务是否应该分开处理

理解线程模型是掌握 Java 并发编程的第一步。从 ThreadPoolExecutor 的执行流程到 ForkJoinPool 的分治策略,再到线程状态的流转,这些知识构成了并发编程的基础框架。