ScheduledThreadPoolExecutor 定时调度增强

ScheduledThreadPoolExecutor 定时调度增强:DelayedWorkQueue 二叉堆延时队列与 Spring 体系实战 🚀 道格·李为什么需要一个能定时的线程池 Java 1.3 引入的 java.util.Timer 是 JDK 最早提供的定时任务工具。但它有两个致命设计缺陷:① 单线程执行——一个任务执行时间过长,后面的所有任务都会延迟;② 异常吞没导致线程终止——任务抛了未捕获异常,Timer 线程静默死亡,剩余任务永远不会执行。第二个问题在生产环境尤其危险——线上定时取消超时订单的任务因为一个 NullPointerException 静默停止,几天后才被发现。 道格·李在设计 JSR 166 时,ScheduledThreadPoolExecutor 是 ThreadPoolExecutor 的直接扩展。它的设计策略是复用线程池的全部管理能力(线程生命周期、拒绝策略、钩子方法),只替换两个关键组件: 任务队列:用 DelayedWorkQueue(基于二叉堆的延时队列)替换 BlockingQueue,任务按触发时间排序,堆顶是最先到期的任务 任务类型:用 ScheduledFutureTask 替换普通的 FutureTask,增加了周期执行模式(固定频率 vs 固定延迟)和下次触发时间的计算逻辑 核心改进:线程池里有 N 个工作线程,一个任务异常不会影响其他线程和任务。Timer 的单线程弱点不再存在。 🏊 ScheduledThreadPoolExecutor 的整体架构 🔗 继承关系与组件概览 ScheduledThreadPoolExecutor 直接继承 ThreadPoolExecutor,在父类基础上替换了三个关键组件: flowchart LR %% ========================================== %% 样式定义 %% ========================================== classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold; classDef branch fill:#2d1a05,stroke:#f59e0b,stroke-width:2px,color:#fde68a,font-weight:bold; classDef leaf fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb; classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; ROOT[ScheduledThreadPoolExecutor\n继承 ThreadPoolExecutor] ROOT --> B1(1. 任务类型替换) B1 --> TASK["📦 ScheduledFutureTask\nextends FutureTask\n+ implements Delayed\n+ 三态 period 模型\n+ sequenceNumber 保序"] ROOT --> B2(2. 队列替换) B2 --> QUEUE["📥 DelayedWorkQueue\n自建二叉堆\n无界阻塞延迟队列\n扩展 leader/follower 模式"] ROOT --> B3(3. 调度方法替代) B3 --> SCHED["⚡ 三个入口方法"] SCHED --> S1["schedule()\n一次性延迟任务\nperiod = 0"] SCHED --> S2["scheduleAtFixedRate()\n固定速率\nperiod > 0"] SCHED --> S3["scheduleWithFixedDelay()\n固定延迟\nperiod < 0"] ROOT --> B4(4. 关闭后行为) B4 --> SHUT["🛑 两个布尔开关"] SHUT --> C1["continueExistingPeriodicTasksAfterShutdown\nshutdown 后是否继续执行周期任务"] SHUT --> C2["executeExistingDelayedTasksAfterShutdown\nshutdown 后是否执行已延迟的任务"] ROOT --> B5(5. 线程数策略) B5 --> SIZE["🔢 maximumPoolSize = Integer.MAX_VALUE\n队列无界,永远不需要额外线程\n仅核心线程数决定并发度"] class ROOT root; class B1,B2,B3,B4,B5 branch; class TASK,QUEUE,SCHED,SHUT,SIZE leaf; class S1,S2,S3,C1,C2 highlight; 五个改进点一句话总结 : ...

九月 4, 2022 · 12 分钟 · 2502 字 · yaomingye

ThreadPoolExecutor 源码解析

ThreadPoolExecutor 源码解析:Worker 机制、生命周期、拒绝策略与动态线程池实践 🚀 道格·李为什么需要一个线程池 Java 1.0 就支持多线程,但管理线程生命周期这件事一直缺少标准方案。开发者每次需要异步执行时,要么 new Thread().start(),要么自己维护一个线程管理队列——前者浪费资源,后者极易出错。 一个线程的创建和销毁是有成本的。JVM 要为每个线程分配栈内存(默认约 1MB),操作系统要为每个线程维护内核线程表项和调度上下文。当并发请求量上来后,频繁创建/销毁线程会导致: 内存压力——大量线程的栈内存吃掉堆外空间 CPU 浪费在上下文切换——线程数远超 CPU 核心数时,CPU 的时间片都消耗在"换人"而不是"干活"上 线程数不可控——请求峰值时线程数无上限增长,最终 OOM 或系统不可用 道格·李在设计 JSR 166 时面对的核心问题是:如何让开发者既能享受多线程的并发收益,又不用直接管理线程的创建和销毁? 答案是把线程抽象为一种可复用的资源——线程池。 线程池的本质是一个"线程 + 任务队列"的组合:核心线程常驻,任务多时创建临时线程分担,任务少时回收空闲线程,任务太多时由拒绝策略兜底。从设计上看,ThreadPoolExecutor 把线程的创建策略(core/max)、存活策略(keepAliveTime)、排队策略(workQueue)和过载策略(rejectedExecutionHandler)全部暴露为可配置参数——这正是道格·李的设计风格:不替开发者做决定,而是把决策权交给调用方。 📐 七大核心参数 ThreadPoolExecutor 最完整的构造器接受 7 个参数: public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) ⚙️ 1. corePoolSize — 核心线程数 线程池中始终存活的线程数量(除非 allowCoreThreadTimeOut 设为 true)。即使这些线程当前空闲,也不会被回收。 关键行为:当提交任务时,即使有空闲的核心线程,只要当前线程数少于 corePoolSize,线程池也会继续创建新的线程——先凑够核心线程数量,再谈复用。这种"先扩容再复用"是出于设计上的简单性:判断是否达到核心线程数的开销远小于判断是否有空闲线程且空闲线程是否可用。 📐 2. maximumPoolSize — 最大线程数 线程池允许创建的最大线程数。只有当工作队列已满且当前线程数不足 maximumPoolSize 时,才会创建超出核心线程数的额外线程。 ...

九月 2, 2022 · 20 分钟 · 4131 字 · yaomingye

ForkJoinPool 源码深度解析

ForkJoinPool 源码深度解析:从分治思想到工作窃取的完整实现 🤔 一、道格·李为什么需要一个"能偷工作"的线程池 Java 5 的 ThreadPoolExecutor 解决了线程复用的问题,但它有一个结构性的局限:所有线程共享一个任务队列。当一个线程提交了子任务后阻塞等待子任务结果,而子任务又在同一个队列里等待被执行时,就会发生线程饥饿——等待的线程占着一个槽位但不干活,队列里的子任务没人执行,形成死锁。 这个问题在递归分治算法(把大问题拆成小问题递归求解)中尤为致命。分治算法天然适合并行——子问题之间互不依赖,可以同时计算。但如果每个线程都把子任务扔到共享队列然后等结果,队列很快就会堆满等待被执行的任务而所有线程都在等。 道格·李在 Java 7 中引入 ForkJoinPool 时,核心创新是工作窃取(Work-Stealing): 每个工作线程有自己的双端队列(Deque),线程从自己的队列头部取任务 当一个线程 fork 子任务时,子任务被 push 到该线程自己的队列 当线程自己的队列空了,它会从其他线程的队列尾部窃取任务来执行 这个设计解决了两个问题:① 递归 fork 的子任务不会堵塞共享队列;② 快线程不会空等——它会偷慢线程的活来干。ForkJoinPool 也是 Java 8 并行流(parallelStream())的底层引擎。 二、设计思想:分治算法 + 工作窃取 📌 2.1 分治算法(Divide-and-Conquer) ForkJoinPool 的设计基础是分治算法(Divide-and-Conquer),其核心过程为三个方面: flowchart TD %% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %% classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb; classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#bfdbfe,font-weight:bold; ROOT[根任务\n问题规模N] ROOT -->|拆分| L1[子任务\n规模N/2] ROOT -->|拆分| R1[子任务\n规模N/2] L1 -->|继续拆分| L2[子任务\n规模N/4] L1 -->|继续拆分| R2[子任务\n规模N/4] R1 -->|继续拆分| L3[子任务\n规模N/4] R1 -->|继续拆分| R3[子任务\n规模N/4] L2 -->|达到阈值\n直接计算| SOLVE1[原子任务] R2 -->|达到阈值\n直接计算| SOLVE2[原子任务] L3 -->|达到阈值\n直接计算| SOLVE3[原子任务] R3 -->|达到阈值\n直接计算| SOLVE4[原子任务] SOLVE1 -->|合并| MERGE1[汇总结果] SOLVE2 -->|合并| MERGE1 SOLVE3 -->|合并| MERGE2[汇总结果] SOLVE4 -->|合并| MERGE2 MERGE1 -->|最终合并| FINAL[最终结果] MERGE2 -->|最终合并| FINAL class FINAL,L1,L2,L3,MERGE1,MERGE2,R1,R2,R3,SOLVE1,SOLVE2,SOLVE3,SOLVE4 process; class ROOT root; 阶段 操作 说明 Divide(拆分) fork() 将大任务递归拆分为小任务,直到达到阈值 Conquer(求解) compute() 对原子任务执行实际计算 Combine(合并) join() 递归汇总所有子任务的结果 📌 2.2 工作窃取算法(Work-Stealing) 普通的 ThreadPoolExecutor 使用单一共享阻塞队列(BlockingQueue),所有线程竞争同一个队列的头元素,存在单点竞争瓶颈。ForkJoinPool 则采用完全不同的设计:每个工作线程维护自己的双端队列(WorkQueue)。 ...

九月 2, 2022 · 20 分钟 · 4180 字 · yaomingye
Cat Radio