线程池实战讲解,加深记忆

📌 线程池配置回顾:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,                           // corePoolSize 核心线程数
    4,                           // maximumPoolSize 最大线程数
    60, TimeUnit.SECONDS,       // 非核心线程空闲存活时间
    new ArrayBlockingQueue<>(2),// 有界任务队列(最多容纳2个任务)
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

你随后提交了 10 个任务:

for (int i = 0; i < 10; i++) {
    executor.execute(() -> {
        System.out.println(Thread.currentThread().getName() + " is executing");
    });
}


🚀 执行过程详解(任务 i = 0 到 9)

提交顺序当前情况执行结果
i = 0核心线程数 < 2 → 创建线程执行任务创建线程1执行
i = 1核心线程数 < 2 → 创建线程执行任务创建线程2执行
i = 2核心线程已满,队列未满 → 入队放入队列
i = 3核心线程已满,队列未满 → 入队放入队列
i = 4核心线程满 + 队列满 → 创建非核心线程创建线程3执行
i = 5核心线程满 + 队列满 → 创建非核心线程创建线程4执行
i = 6核心 + 非核心线程都满,队列也满启动拒绝策略 → 调用者线程执行
i = 7同上 → 调用者线程执行任务主线程执行
i = 8同上 → 调用者线程执行任务主线程执行
i = 9同上 → 调用者线程执行任务主线程执行


🧠 可视化总结(线程池内部状态)

阶段核心线程队列容量非核心线程调用者线程(main)
初始020提交任务
第1~2任务220提交任务
第3~4任务22(满)0提交任务
第5~6任务22(满)提交任务
第7~10任务2自己执行任务


✅ 重点理解:

  • 核心线程(2个)先满

  • 有界队列(2个)接收后两个任务

  • 超过核心 + 队列容量后,线程池最多扩容到 4 个线程

  • 超出最大线程数且队列满时 → 启动拒绝策略

  • CallerRunsPolicy 被触发,主线程(main)阻塞执行任务

    • 注意:这会导致 for 循环的每次提交都会被主线程“卡住”,从而减慢提交速度,形成“背压”


❗ 现实意义:

  • 在实际业务中,例如 Web 应用,若主线程执行任务过久,会导致响应超时

  • 所以要搭配超时控制、监控等保护措施

问题一:第二个任务时,如果第一个线程已完成,线程池是复用它还是新建核心线程?

✅ 答案:不会新建核心线程,会复用已经空闲的核心线程

  • 核心线程池是可复用的,线程池始终尝试复用现有空闲线程来执行新任务;

  • 哪怕核心线程数量未达到 corePoolSize,也不会无脑创建新线程;

  • 只有在所有可用线程都忙,且核心线程数量没达到上限,才会新建核心线程

🧪 举个实际例子:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(2),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

你依次提交两个任务,任务很短(几毫秒就完成):

executor.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " Task1");
});

Thread.sleep(50); // 等待一下,让任务1执行完

executor.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " Task2");
});

执行过程:

  1. 第一个任务启动时,线程池为空 → 创建核心线程 T1 执行 Task1

  2. Task1 很快执行完,T1 变为空闲

  3. 第二个任务提交时,线程池会复用 T1 来执行 Task2,不会新建 T2

✅ 结论 1:线程池优先复用线程,不会因核心线程未满而强制新建线程

问题二:核心线程数(corePoolSize)和最大线程数(maximumPoolSize)有什么区别?

项目核心线程数 corePoolSize最大线程数 maximumPoolSize
定义线程池启动后,始终维持的最小线程数量线程池在高并发时可扩展到的最大线程数
特点永不销毁(除非 allowCoreThreadTimeOut非核心线程超时后会被回收
创建时机一有任务提交就会创建只有当核心线程 + 队列都满后,才会创建
生命周期默认情况下永久存在空闲超过 keepAliveTime 后自动销毁
适用场景保证服务常驻线程高峰时短暂扩容使用

📌 举例说明:

corePoolSize = 2
maximumPoolSize = 5
  • 正常情况下,最多只保留 2 个核心线程运行

  • 当任务量大到撑爆任务队列后,线程池会逐步向最大线程数扩容(最多 5 个)

  • 任务处理完成后,多出的线程(3 个)在空闲一段时间后被销毁

 ✅ 结论 2:

corePoolSize 是线程池“基础承载能力”,maximumPoolSize 是“应急爆发力”。

你可以这样记:

“核心是常驻兵,最大是应急兵。”

问题三:有界队列 vs 无界队列

项目有界队列 (ArrayBlockingQueue)无界队列 (LinkedBlockingQueue)
是否有容量限制✅ 是❌ 否(默认 Integer.MAX_VALUE
当队列满后行为启动拒绝策略 or 扩展线程数不会满,不会触发拒绝策略
是否易 OOM✅ 安全❌ 高并发下极易内存撑爆
maximumPoolSize 的影响会生效:线程数会扩展几乎无效,核心线程之外线程不建
是否适合生产使用✅ 推荐(配合合理容量)⚠️ 小心使用,仅限控制流场景
使用场景举例日志落盘、批量数据处理等少量任务、非常轻量任务场景

Java 线程池中它们怎么影响行为?

我们看下线程池提交任务时的内部逻辑核心段:

if (workerCount < corePoolSize) {
    // 创建核心线程
} else if (queue.offer(task)) {
    // 队列没满:任务排队
} else if (workerCount < maximumPoolSize) {
    // 扩展线程数
} else {
    // 启动拒绝策略
}

✔ 有界队列(如 ArrayBlockingQueue

  • 核心线程数满后,新任务先进入队列;

  • 队列满了,才会考虑创建新线程(到 maxPoolSize);

  • 线程 + 队列都满了,就触发拒绝策略;

  • 安全,但吞吐量受限。

❌ 无界队列(如 LinkedBlockingQueue

  • 核心线程满后,任务总是进入队列;

  • 队列永远不会满,线程数永远不会增加到 maxPoolSize

  • maximumPoolSize 形同虚设

  • 风险:如果任务提交速度 > 处理速度,队列无限增长,最终 OOM。


🚨 真实案例:无界队列导致 OOM

很多开发者一开始使用:

Executors.newFixedThreadPool(n) // 内部是无界队列

然后遇到:

  • 系统运行一段时间后直接崩溃;

  • OutOfMemoryError: Java heap space

  • 用 heap dump 工具发现:大量 Runnable 排在队列中无法被处理。


✅ 正确使用建议

场景建议用什么队列
日志异步落盘(可丢、低延迟)有界队列 + DiscardPolicy
数据分析 / 批量处理任务有界队列 + CallerRuns
核心 Web 请求处理有界队列 + 监控拒绝策略
轻量级任务,且处理速度快可考虑无界队列,但要限流
不知道任务量的动态任务流MQ + 有界线程池


📌 总结一句话

无界队列虽然简单,但极易造成内存溢出;有界队列虽然吞吐有限,但可控、稳定,是生产项目的首选。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值