一、Java 并发编程
Java 并发编程 = 让程序同时做多件事,并保证数据不出错。
1、线程池的核心参数和原理
Java 的ThreadPoolExecutor为例,有 7 个核心参数:
| 参数 | 说明 |
|---|---|
corePoolSize | 核心线程数⭐ |
maximumPoolSize | 最大线程数⭐ |
keepAliveTime | 非核心线程空闲存活时间 |
unit | 存活时间的单位 |
workQueue | 任务等待队列⭐ |
threadFactory | 线程工厂(用于创建线程) |
handler | 拒绝策略(队列和线程池都满时)⭐ |
执行流程如下:
拒绝策略(4种内置)
| 策略 | 行为 |
|---|---|
AbortPolicy(默认) | 直接抛出RejectedExecutionException |
CallerRunsPolicy | 由调用者线程自己执行任务 |
DiscardPolicy | 静默丢弃任务 |
DiscardOldestPolicy | 丢弃队列头部的任务,重新提交当前任务 |
创建一个线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, // corePoolSize 5, // maximumPoolSize 60L, // keepAliveTime TimeUnit.SECONDS, // unit new LinkedBlockingQueue<>(10), // 基于链表的阻塞队列,容量为 10 Executors.defaultThreadFactory(), // 线程工厂 new ThreadPoolExecutor.AbortPolicy() // 拒绝策略 ); // 60L 中的 L 表示这是一个 long 类型的字面量 // SECONDS是存活时间单位,因此非核心线程的空闲存活时间为 60 秒常见误区
队列满后才创建非核心线程(很多人误以为超过 core 就立刻创建)
核心线程也可以被回收:设置
allowCoreThreadTimeOut(true)后,核心线程空闲超时也会被回收Executors工厂方法有风险:newFixedThreadPool:队列无限,可能导致 OOMnewCachedThreadPool:最大线程数无限,高并发时可能创建大量线程
2、IO密集型和CPU密集型的线程池配置
两者的核心区别在于:线程等待的时间占比不同,因此最佳线程数也不同。
简化公式:
CPU密集型:线程数 = CPU核心数 + 1 IO密集型:线程数 = CPU核心数 × 2 (或更多)CPU密集型
特点:任务主要消耗CPU(计算、加密、排序、循环等),线程很少等待。
配置原则:线程数 ≈ CPU核心数,避免过多线程导致频繁上下文切换。
int coreCount = Runtime.getRuntime().availableProcessors(); ThreadPoolExecutor cpuPool = new ThreadPoolExecutor( coreCount, // 核心线程 = CPU核心数 coreCount + 1, // 最大线程 = 核心数+1(留一个余量) 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), // 用有界队列,防止任务堆积过多 Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() );推荐配置:
corePoolSize= CPU核心数maximumPoolSize= CPU核心数 + 1队列大小适中(几百即可)
为什么+1?:即使某个线程因页缺失等短暂阻塞,仍有备用线程继续利用CPU。
IO密集型
特点:任务主要等待IO(数据库查询、HTTP调用、文件读写、网络请求等),CPU大部分时间空闲。
配置原则:线程数可以远大于CPU核心数,让CPU在等待期间去处理其他线程。
int coreCount = Runtime.getRuntime().availableProcessors(); // 方法1: 2倍核心数 ThreadPoolExecutor ioPool1 = new ThreadPoolExecutor( coreCount * 2, // 核心线程 = 2倍核心数 coreCount * 4, // 最大线程 = 4倍核心数 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(200), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy() // 用CallerRunsPolicy削峰 ); // 方法2:更精确的计算(假设IO耗时是CPU的10倍) // 最佳线程数 = CPU核心数 × (1 + 等待时间 / 计算时间) // 线程数 = 核心数 × (1 + 10) = 核心数 × 11 int exactThreads = coreCount * (1 + (ioTime / cpuTime));推荐配置:
corePoolSize= CPU核心数 × 2~4maximumPoolSize= CPU核心数 × 4~8队列可以稍大,但要有限界防止OOM
对比总结
| 维度 | CPU密集型 | IO密集型 |
|---|---|---|
| 线程数 | 少 | 多 |
| corePoolSize | N_cpu | N_cpu × 2~4 |
| maximumPoolSize | N_cpu + 1 | N_cpu × 4~8 |
| keepAliveTime | 较短(秒级) | 较短(秒级) |
| 队列 | 有界,适中 | 有界,可稍大 |
| 拒绝策略 | AbortPolicy | CallerRunsPolicy(削峰) |
CallerRunsPolicy : 由调用者线程自己执行任务
怎么理解:任务提交给线程池,但线程池满了 → 不抛异常、不丢弃任务,而是让 “提交任务的那个线程自己去 run 这个任务”
3、无界队列会导致什么问题?
常见队列类型对比:
| 队列类型 | 特点 | 容量 |
|---|---|---|
LinkedBlockingQueue() | 无界队列,可以无限存任务 | Integer.MAX_VALUE |
LinkedBlockingQueue(10) | 有界队列,最多存 10 个 | 固定大小 |
ArrayBlockingQueue(10) | 基于数组的有界队列 | 固定大小 |
SynchronousQueue() | 不存任务,直接交给线程 | 容量为 0 |
注意:如果用new LinkedBlockingQueue()(不指定容量),队列无界,线程数永远不会超过corePoolSize,而maximumPoolSize参数失效。
使用线程池创建无界队列new LinkedBlockingQueue()(不指定容量)或new LinkedBlockingQueue(Integer.MAX_VALUE)会导致以下严重问题:
1.内存溢出(OOM)最严重
当任务生产速度>消费速度时,队列会无限堆积,直到耗尽内存:
// 危险示例 ThreadPoolExecutor pool = new ThreadPoolExecutor( 2, 10, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>() // 无界队列! ); // 疯狂提交任务(比如每秒1000个,处理能力只有200个/秒) for (int i = 0; i < Integer.MAX_VALUE; i++) { pool.execute(() -> { Thread.sleep(1000); // 任务慢 }); } // 结果:内存持续增长 → OOM → 程序崩溃2. maximumPoolSize 参数失效
核心机制回顾:只有队列满时才会创建新线程(直到达到 max)
ThreadPoolExecutor pool = new ThreadPoolExecutor( 2, 10, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), // 无界队列,永远不满 new ThreadPoolExecutor.AbortPolicy() ); // 提交10000个任务 // 结果:始终只有2个线程在工作,其他8个线程永远不会被创建因为无界队列永远不会满,所以永远不触发创建非核心线程的逻辑。
3.响应时间不可预测
任务积压导致:
延迟从毫秒级 → 秒级 → 分钟级
超时问题:依赖方等不及,上游服务报错
用户体验极差
4. 故障难以排查
系统变慢,但没有报错(无界队列不会抛拒绝异常)
你以为是代码问题,实际上是队列积压
内存缓慢增长,可能几天后才 OOM
排查困难:没有明显的异常堆栈,只有性能下降。
对比示例
| 场景 | 有界队列 (capacity=10) | 无界队列 |
|---|---|---|
| 提交速度 > 处理速度 | 队列满后抛异常或触发拒绝策略 | 队列无限增长 → OOM |
| 最大线程数 | 会创建到 max(队列满时) | 永远不会超过 core(队列永不满足) |
| 问题发现 | 立即抛异常,快速失败 | 慢慢变慢,几天后崩溃 |
| 适用场景 | 生产环境 | 几乎不适用 |
什么情况下可以用无界队列?
极少场景:
任务提交速度稳定且远小于处理速度
核心线程数足够大(等同于最大线程数)
内存足够大 + 监控完善
定时监控队列大小,超过阈值告警
仍是风险较高的设计
4、空闲线程回收机制?
线程池中的空闲线程回收主要针对非核心线程,核心线程默认不会被回收,但也可以配置回收。
1. 非核心线程的回收
触发条件
当线程空闲时间超过keepAliveTime时被回收。
工作原理
ThreadPoolExecutor pool = new ThreadPoolExecutor( 2, // 核心线程数 5, // 最大线程数 60L, // 空闲存活时间 TimeUnit.SECONDS, // 单位:秒 new LinkedBlockingQueue<>(10) ); // 场景:创建了5个线程(2核心+3非核心) // 任务执行完后,非核心线程空闲超过60秒 → 被回收 // 核心线程(2个)不会被回收,一直存活回收过程
任务执行完毕 ↓ 线程从队列获取新任务 poll(keepAliveTime, unit) ↓ 等待keepAliveTime时间 ↓ 没等到新任务 → 超时返回null ↓ 线程判断:当前线程数 > corePoolSize? ↓ 是 该线程被回收(终止)
关键方法:poll(time, unit)会在超时后返回null,触发线程退出。
2. 核心线程的回收
默认情况下,核心线程不会被回收,即使空闲很长时间。
开启核心线程回收
// 设置 allowCoreThreadTimeOut(true) pool.allowCoreThreadTimeOut(true); // 效果:核心线程空闲超过 keepAliveTime 也会被回收回收时机总结
| 线程类型 | 默认行为 | 可配置行为 |
|---|---|---|
| 核心线程 | 永不回收 | 通过allowCoreThreadTimeOut(true)回收 |
| 非核心线程 | 空闲超过keepAliveTime后回收 | 同上 |
5、workQueue的作用,队列满了为什么先入队而不是先扩容线程?
这是一个很好的设计问题。队列先满才扩容是ThreadPoolExecutor的核心设计决策,主要基于以下考虑:
核心原因:线程是昂贵资源
线程创建成本高:
创建线程需要分配栈内存(默认1MB)
涉及操作系统系统调用
线程切换有上下文切换开销
队列存储成本低:
队列中的任务只是对象引用
不创建新线程,开销极小
设计原则:能用便宜的队列缓冲,就不用昂贵的线程资源。
什么时候会先扩容?
有一个例外:SynchronousQueue
// 使用 SynchronousQueue(容量为0) ThreadPoolExecutor pool = new ThreadPoolExecutor( 2, 10, 60L, TimeUnit.SECONDS, new SynchronousQueue<>() // 队列容量为0 );行为变化:
队列容量为0 →
offer()总是立即返回false(除非有消费者在等)实际上变成:直接创建非核心线程(没有"先入队"这一步)
这等同于先扩容,适合任务需要立即执行、不能等待的场景。
总结
| 问题 | 答案 |
|---|---|
| 为什么先入队? | 队列比线程便宜得多 |
| 什么场景下先扩容? | 使用SynchronousQueue时 |
| 什么场景适合先入队? | 有波峰波谷的通用场景 |
| 什么场景需要先扩容? | 延迟极度敏感、任务必须立即执行 |