线程池
拒接策略
线程池工作队列满了有哪些拒接策略?
AbortPolicy (中止策略)
行为:直接抛出
RejectedExecutionException
异常中断任务提交适用场景:对任务完整性要求极高的场景(如金融交易),需配合异常捕获机制
风险:未捕获异常可能导致程序中断
CallerRunsPolicy (调用者运行策略)
行为:将任务回退到提交任务的线程(如主线程)直接执行
适用场景:生产速度远高于消费速度时自然限流,允许短暂阻塞主线程的非实时任务
注意事项:避免关键线程(如 UI 主线程)被阻塞
DiscardPolicy (静默丢弃策略)
行为:无提示丢弃新任务,不抛异常也不执行
适用场景:允许任务丢失的非关键场景(如实时监控采样)
风险:需监控丢弃任务量,避免数据不一致
DiscardOldestPolicy (弃老策略)
行为:丢弃队列中最旧任务,重试提交当前新任务
适用场景:新任务优先级高于旧任务(如实时消息覆盖历史数据)
风险:可能丢弃重要旧任务,需评估业务容忍度
自定义拒绝策略
实现方式:实现
RejectedExecutionHandler
接口扩展逻辑典型方案:
持久化存储:任务存入数据库/Redis 后续处理
异步重试:延迟后重新提交或转移至备用线程池
告警通知:触发监控并记录任务详情
适用场景:需保障任务最终执行的业务(如订单支付)
策略选择原则
任务重要性:关键任务优先用
CallerRunsPolicy
或自定义策略系统容忍度:允许丢失用
DiscardPolicy
,需时效性用DiscardOldestPolicy
资源限制:内存敏感场景避免无界队列,明确队列容量
监控配套:静默丢弃策略需加强队列长度和拒绝次数监控
核心线程数
核心线程数设置为 0 可不可以?
可以设置但需谨慎权衡
技术上允许将核心线程数设为 0,线程池通过非核心线程处理任务
任务提交逻辑:
当前工作线程为 0 时创建非核心线程执行任务
已有非核心线程且队列未满时,任务入队等待
适用场景:
极低频非关键任务(如日志清理),避免常驻线程占用资源
突发流量后需快速释放资源的临时任务(如活动促销统计)
潜在风险与限制
线程无法复用:每次任务可能触发新建线程,增加创建/销毁开销
队列容量敏感
无界队列(如
LinkedBlockingQueue
)可能导致 OOM有界队列需精确设置容量防止任务堆积
响应延迟:首次任务需等待线程创建,影响实时性要求高的场景
替代优化方案
动态核心线程:通过
allowCoreThreadTimeOut(true)
允许核心线程超时回收,保留复用能力弹性线程池
根据 QPS 动态调整
corePoolSize
(如夜间降为 0,高峰期恢复)结合监控告警自动扩容/缩容(如 Prometheus + 动态配置中心)
实践建议
稳定性要求高的系统(如支付服务)避免核心线程数为 0,优先动态调整策略
测试环境需验证线程创建速率和任务响应时间,防止生产环境性能瓶颈
shutdown & shutdownNow
线程池中 shutdown (),shutdownNow() 这两个方法有什么作用?
shutdown() 的作用
新任务处理:立即拒绝新任务提交,触发拒绝策略(如抛
RejectedExecutionException
)现有任务处理:等待已提交任务(包括执行中和队列中的)全部完成
状态变化:将线程池状态从
RUNNING
变为SHUTDOWN
,最终进入TERMINATED
线程中断:仅中断空闲线程,不干扰执行中的任务
shutdownNow() 的作用
新任务处理:立即拒绝新任务提交。
现有任务处理
尝试中断所有正在执行的任务(通过
interrupt()
)清空队列并返回未执行任务列表(
List<Runnable>
)
状态变化:将状态从
RUNNING
改为STOP
,最终进入TERMINATED
线程中断:强制中断所有工作线程,无论是否空闲
关键差异对比
新任务处理:均拒绝新任务
现有任务处理
shutdown()
等待任务完成shutdownNow()
中断任务并丢弃未执行任务
线程中断范围
shutdown()
仅中断空闲线程shutdownNow()
中断所有线程
返回值
shutdown()
无返回值shutdownNow()
返回未执行任务列表
适用场景
shutdown()
:优雅停机(如服务正常退出)shutdownNow()
:紧急终止(如死锁或资源耗尽)
使用建议
组合调用:先调用
shutdown()
等待任务完成,若超时未终止再调用shutdownNow()
,需捕获InterruptedException
并处理。任务容错设计:在任务代码中检查中断状态,确保能响应中断请求
注意事项
资源泄漏:未正确关闭线程池可能导致线程和资源(如数据库连接)无法释放
队列影响
无界队列使用
shutdown()
可能导致 OOMshutdownNow()
清空队列可缓解内存压力
线程任务撤回
提交给线程池中的任务可以被撤回吗?
通过
Future
对象取消任务提交任务后返回
Future
对象,调用cancel(boolean mayInterruptIfRunning)
可尝试取消任务参数作用
mayInterruptIfRunning = true
:若任务已执行,触发线程中断(需任务代码响应中断)mayInterruptIfRunning = false
:仅取消未开始执行的任务
返回值
true
:任务成功取消(未开始或已响应中断)false
:任务已完成或无法取消
不同任务状态下的撤回结果
任务未开始执行:
cancel()
可成功移除队列中的任务任务正在执行:依赖任务代码的中断响应逻辑(如检查
isInterrupted()
或捕获InterruptedException
)任务已完成:无法撤回,
cancel()
返回false
线程池实现的影响
ThreadPoolExecutor
的局限性无界队列(如
LinkedBlockingQueue
)中的任务无法直接撤回,需通过Future.cancel()
remove(Runnable task)
方法可移除队列中未执行的任务,但需明确任务引用
定时任务(
ScheduledThreadPool
)ScheduledFuture.cancel()
可终止周期性任务,已开始的单次任务仍需中断响应逻辑
实践建议
代码容错设计:任务逻辑中定期检查中断状态,确保能响应取消请求
组合关闭策略
先调用
shutdown()
等待任务完成,超时后使用shutdownNow()
强制终止
避免无界队列:使用有界队列(如
ArrayBlockingQueue
)减少无法撤回任务的风险
使用场景
奇偶数
多线程打印奇偶数,怎么控制打印的顺序
基础同步锁方案
同步机制:使用
synchronized
保证原子操作,避免竞态条件双重检查机制
外层
while(num <= 100)
控制整体循环内层
while(num%2 ==0)
实现条件等待
线程协作
wait()
释放锁并暂停当前线程notify()
唤醒等待线程
标志位优化方案
显式状态控制:通过
isOddTurn
布尔标志管理奇偶切换性能优化
避免频繁计算奇偶性,提升执行效率
便于扩展多线程交替逻辑(如三线程 ABC 交替)
高级锁机制方案
精细控制:使用
ReentrantLock
替代synchronized
,提供更灵活的锁管理条件队列
通过
Condition
对象(oddCond
和evenCond
)分别管理奇偶线程等待队列signal()
精准唤醒目标线程。
实现要点总结
同步机制选择:简单场景用
synchronized
,复杂场景用ReentrantLock
条件判断:必须使用
while
循环检查条件,避免虚假唤醒数值递增:在同步代码块内完成修改,保证原子性
终止条件:循环外设置退出检测(如
num > 100
),防止数值越界异常处理:捕获
InterruptedException
并确保锁最终释放
调试与扩展
调试建议
通过
Thread.setName()
设置线程名称,便于日志追踪使用
jstack
检测死锁,VisualVM
监控线程状态
扩展应用场景
多阶段交替:修改为三个线程交替打印(如 ABCABC 序列)
动态调整上限:通过
volatile
变量实现运行时修改最大值分布式扩展:结合消息队列(如 Kafka)实现跨进程顺序控制
单例模型
单例模型既然已经用了 synchronized,为什么还要在加 volatile?
解决指令重排序问题
对象实例化操作
instance = new Singleton()
在 JVM 中分为三步分配内存空间
调用构造函数初始化对象
将对象引用赋值给变量
无
volatile
时可能发生指令重排序(如顺序变为 ①→③→②),导致其他线程获取未初始化完成的实例volatile
通过内存屏障禁止指令重排序,确保初始化顺序为 ①→②→③,避免「半初始化」对象
保证可见性
当首个线程完成实例化后,
volatile
强制将最新值刷新到主内存,其他线程立即感知instance
非空,避免重复创建仅用
synchronized
时,其他线程可能因本地缓存未更新而误判instance
为空
双重检查锁定的完整性
第一次判空:减少锁竞争,仅实例未初始化时进入同步块
第二次判空:防止多个线程通过第一次检查后重复初始化
volatile
配合:确保第二次判空时读取最新值,避免指令重排序或可见性问题导致判空失效
性能与安全的平衡
synchronized
保证原子性,但无法单独解决指令重排序和可见性问题volatile
以较低性能代价(内存屏障)补充synchronized
的不足,使 DCL 单例高效且线程安全
线程等待
3 个线程并发执行,1 个线程等待这三个线程全部执行完在执行,怎么实现?
使用 CountDownLatch
核心机制:创建初始值为 3 的计数器,三个线程执行完毕后调用
countDown()
减少计数等待线程:通过
await()
阻塞至计数器归零后执行后续任务
线程 Join 方法
实现方式:在等待线程中依次调用三个并发线程的
join()
,使其阻塞直至所有目标线程结束注意事项
需按顺序先启动并发线程再启动等待线程
适用于简单场景,代码耦合度较高
CompletableFuture 组合
核心方法:通过
CompletableFuture.allOf()
等待所有异步任务完成优势:支持异步任务组合,可链式调用后续操作(如
thenRun()
)
线程池与 awaitTermination
实现流程
将三个任务提交至线程池
调用
shutdown()
关闭线程池后,使用awaitTermination()
等待任务完成
优势:适合批量任务管理,可设置超时时间防止无限等待
信号量 (Semaphore)
控制逻辑
初始化三个许可,等待线程需调用
acquire(3)
获取全部许可后执行每个并发线程执行后调用
release()
释放许可
适用场景:支持动态调整等待线程数量,灵活性较高
方案选择建议
轻量级场景:优先选择
CountDownLatch
或CompletableFuture
,代码简洁且控制精准批量任务管理:推荐线程池的
awaitTermination
,便于扩展和资源管理动态调整需求:使用
Semaphore
可灵活控制许可数量异常处理:所有方案需注意线程泄漏和死锁风险,确保资源释放
并发读写
假设两个线程并发读写同一个整型变量,初始值为零,每个线程加 50 次,结果可能是什么?
可能的取值范围
最低结果:0(极端情况下所有加操作被覆盖)
最高结果:100(理想线程串行执行)
实际常见值:50~100 之间波动,可能更小(如交替执行多次覆盖修改)
原因分析
非原子操作:
n += 1
包含读取、修改、写入三步,可能被线程切换打断示例:线程 A 读
n=0
→ 线程 B 读n=0
→ 均写入n=1
,导致两次操作仅生效一次
缓存可见性问题:多核 CPU 缓存未同步,线程可能读取旧值(如线程 A 修改后未刷回主存,线程 B 仍用
n=0
计算)指令重排序:编译器或 CPU 调整指令顺序,延迟写入操作加剧数据不一致
解决方案
同步锁
使用
synchronized
或ReentrantLock
保证临界区代码原子性
原子变量
用
AtomicInteger
替代普通整型变量,通过incrementAndGet()
的 CAS 机制保证原子性,性能优于锁
线程安全容器
如
ConcurrentHashMap
提供细粒度并发控制,但本场景更适用原子变量
高级
线程池种类
FixedThreadPool (固定大小线程池)
核心参数:
corePoolSize = maximumPoolSize
,无界队列LinkedBlockingQueue
特点:线程数固定,适用于任务量稳定场景(如 Web 服务器请求处理)
风险:无界队列可能导致 OOM
CachedThreadPool (可缓存线程池)
核心参数:核心线程数 0,最大线程数
Integer.MAX_VALUE
,同步队列SynchronousQueue
特点:动态扩容线程,空闲线程 60 秒回收(适合短期异步任务)
风险:线程过多可能耗尽资源
SingleThreadExecutor (单线程池)
核心参数:核心线程数 1,无界队列
LinkedBlockingQueue
特点:保证任务顺序执行(如日志处理、数据库操作)
ScheduledThreadPool (定时任务线程池)
核心参数:固定核心线程数,延迟队列
DelayedWorkQueue
特点:支持延时/周期性任务(如定时备份、心跳检测)
WorkStealingPool (工作窃取线程池)
核心参数:基于
ForkJoinPool
,默认线程数等于 CPU 核数特点:任务窃取机制,适合并行计算(如分治算法、图像处理)
自定义线程池
核心参数:通过
ThreadPoolExecutor
定制核心线程数、队列类型及拒绝策略适用场景:高并发系统灵活平衡资源与效率
其他补充
ForkJoinPool:专为分治任务设计(如递归计算、大规模数据处理)
拒绝策略:支持
AbortPolicy
(抛异常)或DiscardOldestPolicy
(丢弃旧任务)等
线程池参数
corePoolSize (核心线程数)
线程池保持活跃的最小线程数,空闲时不会被销毁(除非设置
allowCoreThreadTimeOut=true
)新任务提交时优先创建核心线程处理,直到达到阈值
maximumPoolSize (最大线程数)
允许创建的最大线程总数(核心线程 + 非核心线程)
当队列已满且线程数小于该值时,创建非核心线程
keepAliveTime (空闲线程存活时间)
非核心线程空闲时的最大存活时间,超时后销毁
与
unit
配合使用,默认不影响核心线程
unit (时间单位)
定义
keepAliveTime
的时间单位(如秒、毫秒)
workQueue (任务队列)
存储待执行任务的阻塞队列,影响任务调度逻辑
常见类型:
LinkedBlockingQueue
(无界队列,可能 OOM)ArrayBlockingQueue
(有界队列,推荐使用)SynchronousQueue
(直接传递任务)
threadFactory (线程工厂)
自定义线程创建方式(命名规则、优先级、守护线程属性)
默认使用
Executors.defaultThreadFactory()
handler (拒绝策略)
线程和队列均满时处理新任务的策略
常用策略:
AbortPolicy
(默认,抛异常)CallerRunsPolicy
(提交线程直接执行)DiscardPolicy
(静默丢弃)DiscardOldestPolicy
(丢弃最旧任务)
注意事项
队列选择原则:
CPU 密集型任务推荐有界队列(如
ArrayBlockingQueue
)I/O 密集型任务可考虑无界队列(需防范 OOM)
参数联动规则:
仅当
workQueue
已满且线程数 <maximumPoolSize
时创建非核心线程使用无界队列时
maximumPoolSize
无效
推荐实践:手动创建
ThreadPoolExecutor
控制队列边界,避免使用Executors
默认实现
线程池设计
任务类型决定核心线程数
CPU 密集型任务(如加密计算):
corePoolSize = CPU 核数 + 1
(如 8 核设为 9 线程)I/O 密集型任务(如网络请求):
corePoolSize ≈ 2 * CPU 核数
(如 8 核设为 16)混合型任务:
corePoolSize = Ncpu * Ucpu * (1 + W/C)
,其中W
为任务等待时间,C
为计算时间
队列选择与容量设计
无界队列(
LinkedBlockingQueue
):适用短耗时任务,明确设置容量防止 OOM(如LinkedBlockingQueue(2000)
)有界队列(
ArrayBlockingQueue
):容量计算为(corePoolSize / 任务平均耗时) * 最大容忍响应时间
同步队列(
SynchronousQueue
):需高实时性,搭配maximumPoolSize
设为较大值(如 50-100)
线程池扩容与收缩规则
最大线程数:通常设为核心数的 1.5-3 倍(如电商场景核心 85,最大 150)
空闲线程回收:
keepAliveTime
设为 30-60 秒,突发流量可延长至 2-5 分钟动态调整:根据 QPS 监控自动扩容(
新核心数 = 当前 QPS * 平均耗时 / 1000 * 0.8
)
拒绝策略与容错机制
CallerRunsPolicy:允许主线程降级处理(如日志记录)
DiscardOldestPolicy:新任务优先级高(如实时消息覆盖)
自定义策略:
持久化存储:任务存数据库/Redis 后续处理
异步重试:延迟后提交或转移至备用线程池
监控与调优实践
监控指标:活跃线程数、队列堆积量、任务平均耗时(推荐 Prometheus 实时监控)
压测验证:某电商优化后 QPS 从 4200 提升至 8500,错误率从 15% 降至 0.2%
弹性设计:容器水平扩容(如 K8s)与线程池参数调优结合应对突发流量
避坑指南
避免
corePoolSize = maximumPoolSize
(丧失突发能力)明确队列容量(禁用默认无界队列)
CPU 密集型任务勿用
CallerRunsPolicy
(可能阻塞主线程)
工作原理
核心组成与初始化
线程池管理器:负责创建、销毁线程及任务调度,维护线程池生命周期
任务队列:存储待执行任务,常用阻塞队列(如
LinkedBlockingQueue
),队列满时触发拒绝策略工作线程:核心线程常驻处理任务,非核心线程队列满时动态创建,空闲超时后销毁
线程工厂与拒绝策略:自定义线程属性(如命名规则),定义队列满时的处理逻辑(如抛异常)
任务处理流程
任务提交:通过
submit()
或execute()
提交Runnable
/Callable
任务资源分配
线程数 < 核心线程数:直接创建新线程执行
核心线程已满:任务入队等待
队列满且线程数 < 最大线程数:创建救急线程处理
队列和线程均满:触发拒绝策略(如
AbortPolicy
)
任务执行:工作线程从队列取任务执行,完成后等待新任务
线程池状态管理
状态标识:通过
ctl
变量(高3位状态,低29位线程数)维护:RUNNING:正常接收并执行任务
SHUTDOWN:不再接收新任务,处理存量任务
STOP:中断所有任务,丢弃未处理任务
TERMINATED:线程池完全终止
状态转换:
shutdown()
进入 SHUTDOWN,shutdownNow()
进入 STOP,完成后转为 TERMINATED
性能优化机制
线程复用:避免频繁创建/销毁,减少上下文切换(每次 1-10 微秒)
动态伸缩:根据任务量调整救急线程数量,空闲超时后回收
队列选择
CPU 密集型:有界队列(如
ArrayBlockingQueue
)I/O 密集型:无界队列(如
LinkedBlockingQueue
)
拒绝策略适配:电商系统采用
CallerRunsPolicy
由提交线程执行任务
典型应用场景
Web 服务器:Tomcat 用固定核心线程数(如200)处理 HTTP 请求
异步任务处理:日志写入用缓存线程池(
CachedThreadPool
)动态扩容定时任务调度:
ScheduledThreadPool
支持延迟/周期性任务(如数据同步)数据库连接池:复用连接减少建立开销(类比线程池设计)。
等待队列
为啥 Java 线程池核心线程满后,入队列等待,Tomcat 核心满后不入队列等待的原因
设计目标差异
Java 线程池:面向 CPU 密集型任务,优先用队列缓冲任务减少线程创建开销。核心线程满后任务入队,队列满时才创建非核心线程。
Tomcat 线程池:针对 I/O 密集型场景优化,优先创建新线程降低延迟,核心满后直接扩容线程至最大值,队列仅作最后缓冲
队列行为实现机制
Java 默认队列:使用
LinkedBlockingQueue
,核心线程满后直接入队,无界队列可能导致 OOMTomcat 自定义队列
重写
offer()
方法:当已提交任务数超过活跃线程数时返回false
触发创建新线程仅在线程达最大值或队列满时才真正入队
线程创建策略对比
Java 线程池:严格遵循「核心线程→队列→非核心线程」流程,队列未满不扩容
Tomcat 线程池:核心线程满后,若提交任务数大于当前线程数,立即创建新线程直至最大值
性能与资源权衡
Java 队列优先:适合耗时长的 CPU 运算任务,减少线程切换损耗
Tomcat 线程优先:避免队列延迟,快速响应突发流量(如秒杀请求),默认限制队列长度(
acceptCount=100
)防 OOM
参数配置体现差异
Java 参数:
corePoolSize
与maximumPoolSize
分离,依赖队列容量控制资源Tomcat 参数
maxThreads
限制线程总数,acceptCount
限制队列容量minSpareThreads
预创建核心线程,减少首次请求延迟
内置线程池
为什么不推荐用内置线程池
资源耗尽风险
无界队列导致 OOM
FixedThreadPool
和SingleThreadExecutor
使用LinkedBlockingQueue
,任务提交速度过快时队列无限堆积可能引发内存溢出。ScheduledThreadPool
的延迟队列容量为Integer.MAX_VALUE
,周期性任务堆积存在 OOM 隐患
无界线程池问题
CachedThreadPool
允许创建Integer.MAX_VALUE
线程,高并发下可能耗尽 CPU/内存资源
配置灵活性与容错能力不足
参数固定化
内置线程池的队列类型、核心线程数等参数无法动态调整,难以适配突发流量或资源敏感型任务
默认拒绝策略单一
默认使用
AbortPolicy
(抛异常),缺乏降级、重试等容错机制,可能引发服务雪崩
可维护性与监控缺陷
线程命名模糊
内置线程池生成的线程名称(如
pool-1-thread-n
)无业务含义,增加排查难度
缺乏监控支持
无法直接获取活跃线程数、队列堆积量等关键指标,需额外开发监控逻辑
生产环境最佳实践
推荐替代方案
使用
ThreadPoolExecutor
手动配置:有界队列:如
ArrayBlockingQueue
控制内存风险合理线程数:根据 CPU 核数和任务类型(CPU/I/O 密集型)设置
corePoolSize
和maximumPoolSize
自定义拒绝策略:结合
CallerRunsPolicy
或降级逻辑(如任务存入外部队列)
线程工厂与命名
自定义
ThreadFactory
为线程添加业务标识,便于日志追踪
生命周期管理
通过
shutdown()
和shutdownNow()
组合实现优雅关闭,避免线程泄漏
评论区