尧图精选

ScheduledExecutorService深度拆解:三大方法与线程池避坑指南

🕒 发布时间:2026/9/13 1:59:48 📁 来源:尧图网络
做 Java 开发这么多年定时任务算是刚需中的刚需。不管你是写订单超时关单、缓存定时刷新还是夜里跑报表都绕不过“到点执行”“每隔多久执行一次”这种需求。而说到 JDK 自带的定时任务方案ScheduledExecutorService 就是最核心的那一个也是面试八股文里出现频率极高的考点。很多人用是会用但问到底层这几个方法有什么区别、任务抛异常之后会怎样、线程数到底怎么设置一下子就说不上来了。这篇文章我就把 ScheduledExecutorService 里的 schedule、scheduleAtFixedRate、scheduleWithFixedDelay 这几个方法彻底拆开从使用场景、源码实现到落地踩坑一次讲清楚。如果你正在准备 Java 面试或者写业务代码时需要选一个合适的定时任务方案这篇文章应该能帮你省不少事。1. 定时线程池到底是什么先搞清需求场景1.1 一个典型的定时任务场景先不看代码想一想你实际工作中遇到的定时需求大概有几类。第一类是“延迟执行”型。比如用户下单后 30 分钟没付款系统要自动关单。这个场景不是周期性的而是“等 30 分钟之后执行一次”。第二类是“固定频率”型。比如每隔 5 分钟刷新一次本地缓存或者每隔 10 秒拉一次数据库配置。第三类是“固定间隔”型。比如凌晨跑完日报生成任务之后等 10 分钟再执行下一个数据推送任务要求上一次任务彻底跑完才开始计时。这三种需求对应的 API 其实不同很多人一把梭全用 scheduleAtFixedRate结果任务一多、一慢各种奇怪的问题就出来了。ScheduledExecutorService 是 Java 并发包 java.util.concurrent 里专门干这个事的接口它提供了一套基于线程池的定时调度能力核心实现类是 ScheduledThreadPoolExecutor。它的工作方式本质上还是线程池那一套你提交一个任务它把任务包装成一个延迟任务对象放进一个延迟阻塞队列里工作线程从队列里取任务执行。但又跟普通的线程池不太一样因为它内部处理了“下一次执行时间”的计算逻辑这是题目的关键也是面试官最爱深挖的地方。1.2 为什么说 Timer 已经被淘汰了很多老项目里还能看到 java.util.Timer 的痕迹早期 JDK 定时任务基本靠它。但 Timer 的问题相当致命它内部只有单线程任何一个任务抛了未捕获异常整个线程就挂了其他所有定时任务全部失效而且还没有任何提示。我在一个老项目里亲眼见过一个定时任务凌晨三点跑挂了后面所有依赖 Timer 的任务全部静默停止直到业务方发现数据好几天没更新才意识到出事了。Timer 还有另外的槽点它对系统时钟变化敏感系统时间一改调度就可能乱掉。而 ScheduledThreadPoolExecutor 基于线程池实现一个任务异常不会影响其他任务多个任务可以并行执行线程数可调还支持 ScheduledFuture 取消任务。从哪个角度看Timer 都该被换掉了。所以现在的主流方案就是 ScheduledExecutorService。但这还不够因为同一接口下的三个方法底层语义完全不同用错了地方后果很直接——任务会不定期“失踪”或者执行频率跟你想的完全不是一回事。2. 三个核心方法一次讲透2.1 schedule只执行一次适合延迟任务先看最简单的 schedule 方法。它的作用是延迟一段时间后执行一次任务不重复执行。ScheduledExecutorService scheduler Executors.newScheduledThreadPool(2); // Runnable 版本延迟 5 秒执行一次 scheduler.schedule(() - { System.out.println(5秒后执行一次执行时间 System.currentTimeMillis()); }, 5, TimeUnit.SECONDS);这里有几个关键点。第一个参数是任务本身第二个参数是延迟时间第三个参数是时间单位。比如 5 秒就写 5 和 TimeUnit.SECONDS比写成 5000 毫秒可读性强得多。schedule 还有另一个重载版本接收 Callable 参数区别在于它可以拿到返回值ScheduledFutureString future scheduler.schedule(() - { Thread.sleep(2000); return 任务执行结果; }, 3, TimeUnit.SECONDS); // 阻塞等待任务完成并获取返回值 String result future.get(); System.out.println(拿到返回值 result);这个版本适合那些需要等任务执行完拿结果再继续处理的场景。比如你要预加载一批数据最多等 3 秒拿到结果后继续走业务流程。注意如果你的业务场景是“只执行一次”就用 schedule不要绕到周期方法里去做特殊处理。返回的 ScheduledFuture 对象还支持取消任务比如ScheduledFuture? future scheduler.schedule(() - { System.out.println(这条任务可能不会执行); }, 10, TimeUnit.SECONDS); // 5 秒后反悔了取消任务 Thread.sleep(5000); future.cancel(false);cancel(false) 表示如果任务还没开始执行就取消掉如果已经在执行中了就让它执行完。这个参数在很多场景下很关键比如订单超时后用户手动支付成功了你就可以取消掉那个关单任务。2.2 scheduleAtFixedRate按固定频率执行自带“补执行”机制再来看周期任务里最常见、也最容易出问题的 scheduleAtFixedRate。ScheduledExecutorService scheduler Executors.newScheduledThreadPool(2); // 初始延迟 1 秒之后每隔 3 秒执行一次 scheduler.scheduleAtFixedRate(() - { System.out.println(定时任务执行 System.currentTimeMillis()); }, 1, 3, TimeUnit.SECONDS);方法签名是 scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)。它表达的意思是任务第一次执行前等 initialDelay 这么久之后按照固定的时间间隔 period 周期性执行。这里的核心概念是“按固定频率”。换句话说它管的是“每过 period 就应当启动一次”。假设你设置 period 为 3 秒任务每次执行耗时 1 秒那么执行的节奏是第 1 秒执行第一次第 4 秒执行第二次第 7 秒执行第三次看起来非常规律。但如果任务执行时间超过了 period情况就不一样了。比如任务本身要跑 5 秒period 设置的是 3 秒第一次任务在第 1 秒开始、第 6 秒结束。按照固定频率第 4 秒就应该启动下一次了但线程不够、或者任务还没结束不能并发跑同一个任务怎么办答案是上一次任务结束后它会立即执行下一次中间没有等待时间。所以任务会变成第 1 秒开始、第 6 秒结束、第 6 秒马上开始下一次、第 11 秒结束…… 相当于实际执行间隔退化成接近任务本身的耗时那个 3 秒的 period 形同虚设。这不算 bug而是 scheduleAtFixedRate 的设计方式。它追求的是“尽量补足执行次数”实现方式是在计算下一次执行时间时以上一次理论执行时间为基准去加 period而不是以上一次实际结束时间去加。关于这一点第三部分源码解析里会再验证。这个方法的典型适用场景是对时间点敏感的任务比如心跳上报、缓存定时刷新、指标采样等。这类任务要求的是“每隔多少秒做一次”哪怕某次慢了点也希望尽可能补上执行次数而不是干等固定间隔。2.3 scheduleWithFixedDelay固定延迟跑完再等再看第三个方法 scheduleWithFixedDelay。ScheduledExecutorService scheduler Executors.newScheduledThreadPool(2); // 初始延迟 1 秒每次执行完毕后再等 3 秒才开始下一次 scheduler.scheduleWithFixedDelay(() - { System.out.println(任务开始 System.currentTimeMillis()); }, 1, 3, TimeUnit.SECONDS);方法签名是 scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)。它表达的意思是任务第一次执行前等 initialDelay之后每次任务执行完再过 delay 时间才执行下一次。关键差别在于这里管的是“上一个任务结束到下一个任务开始之间的间隔”。不管任务本身跑了 1 秒还是 5 秒它都严格保证任务结束之后再休息 3 秒才开始下一次。用上面的例子来对比任务执行耗时 5 秒delay 设置为 3 秒。第一次任务第 1 秒开始、第 6 秒结束。第二次任务最早也要等到第 9 秒才开始。这才是一般人直觉里的“每隔 3 秒跑一次”的稳定语义——任何一次执行慢了只会影响下一次的开始时间但不会导致任务连续追跑。这个方法的适用场景也很明显数据同步、报表生成、批量导出、邮件推送这类任务。特点是你希望两次任务之间有稳定的空闲间隔防止上一个任务还没处理完下一个就冲上来了而且单次任务耗时波动比较大时fixed delay 能天然地把时间拉开让任务边界清晰。2.4 一张表理清两个周期方法的差异与其背定义不如把两个周期方法直接放到表格里对比这样记忆更清晰。对比项scheduleAtFixedRatescheduleWithFixedDelay定时基准以“任务开始时间”为基准以“任务结束时间”为基准两次任务的间隔计算每次开始时间间隔固定为 period每次开始时间间隔 上一次耗时 delay任务耗时超过周期结束后立即补跑可能连续执行结束后再等 delay不补跑适用场景心跳、缓存刷新、指标采样数据同步、报表、推送任务实际效果尽量补足执行次数保证两次任务之间最小休息间隔再举个例子方便理解。设 period/delay 都为 5 秒任务本身执行 2 秒。scheduleAtFixedRate 的执行节奏大致是0s 开始2s 结束5s 开始7s 结束10s 开始12s 结束。非常规律。但任务执行时间变成 8 秒后节奏彻底变了0s 开始8s 结束。本来 5s 就该触发第二次但任务还没结束所以 8s 结束后立即开始第二次8s 开始16s 结束。第三次本该是 10s 触发结果被推到了 16s。后面实际变成每次间隔约 8 秒那 5 秒的 period 毫无存在感。同样场景换成 scheduleWithFixedDelay节奏是0s 开始8s 结束休息 5 秒13s 开始21s 结束再休息 5 秒26s 开始。每次都严格保证 5 秒的休息期。理解了这两者的实际时间线面试时基本就不会再被绕晕了。3. 从源码看这三个方法的实现差异3.1 ScheduledFutureTask 和那个带方向的 period光知道区别还不够容易被面试官一个问题问倒。比如他问“你说 scheduleAtFixedRate 任务超时会补跑JDK 底层怎么实现的”这个时候就得打开源码了。ScheduledThreadPoolExecutor 内部把所有任务都包装成了一个 ScheduledFutureTask 对象。这个对象里有一个关键的 period 字段用带符号的 long 表示任务类型。规则是这样的period 等于 0表示一次性任务period 大于 0表示 scheduledAtFixedRate 的周期任务period 小于 0表示 scheduledWithFixedDelay 的周期任务。对应的源码片段大致是private class ScheduledFutureTaskV extends FutureTaskV implements RunnableScheduledFutureV { /** 触发时间纳秒 */ private long time; /** period 0 表示一次性任务 0 固定频率 0 固定延迟 */ private final long period; ... }再看看两个周期方法内部是怎么构造这个任务的public ScheduledFuture? scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { ... ScheduledFutureTaskVoid sft new ScheduledFutureTaskVoid(command, null, triggerTime(initialDelay, unit), unit.toNanos(period)); // period 为正数 ... } public ScheduledFuture? scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { ... ScheduledFutureTaskVoid sft new ScheduledFutureTaskVoid(command, null, triggerTime(initialDelay, unit), unit.toNanos(-delay)); // period 为负数 ... }注意scheduleAtFixedRate 构造对象时传的是正的 periodscheduleWithFixedDelay 传的是负的 delay。同一个字段用正负号来表示不同的调度语义这就是底层判断的“方向标”。这段代码虽然短但它是整个定时线程池设计里最精彩的地方之一。3.2 setNextRunTime为什么超时会立即补跑再来看看真正决定下一次执行时间的 setNextRunTime 方法。void setNextRunTime() { long p period; if (p 0) time p; // fixed rate在旧触发时间基础上累加 else time triggerTime(-p); // fixed delay以当前时间为基准加上延迟 }这行代码就是整个差异的核心。当 p 0也就是 fixed rate 模式下一次执行时间是在旧的 time 字段上直接累加 p而不是基于“当前时间”计算。time 字段表示的是“理论上计划执行的触发时间”所以即使任务因为执行时间过长而延后了下一次也是从理论触发时间再往后加一个 period。如果当前时间已经超过了这个新的触发时间任务从队列里一取出来就会立刻执行这也就是超时后补跑的根本原因。当 p 0也就是 fixed delay 模式下一次执行时间是在“当前时间”的基础上加上延迟时间。而这里的“当前时间”指的是上一次任务执行完、重新进入队列那一刻的时间。所以天然保证任务结束后至少休息 delay 时间下一次才会到点。这两个分支本质上就是两种不同的时间计算策略一个向前看计划一个向后看实际。面试官问“为什么 fixed rate 在任务超时会连续跑”你把这个源码逻辑讲出来他基本就点头了。3.3 DelayedWorkQueue为什么 maximumPoolSize 形同虚设ScheduledThreadPoolExecutor 还有一个很多新手没注意到的点它的队列是 DelayedWorkQueue一个无界延迟队列。它跟普通线程池的差异在于任务的排序不是按提交顺序而是按触发时间排序最近的排在最前面。而且因为这个队列是无界的它根本不会触发线程池的“队列满了、创建非核心线程”这条路径。换句话说ScheduledThreadPoolExecutor 的 workQueue 永远塞不满maximumPoolSize 这个参数不管设多少都几乎不会起作用真正决定并发能力的只有 corePoolSize。看看它的构造方法public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }maximumPoolSize 直接给了 Integer.MAX_VALUE但因为队列无界这个值实际上只是个摆设。所以你在设置线程池时真正要考虑的是核心线程数。默认情况下 Executors.newScheduledThreadPool 只是把核心线程数设置成你传入的值其他参数都是这套默认值。这带来的一个实际后果就是如果你的 corePoolSize 设为 1又提交了 5 个周期任务不管这几个任务的周期多长它们本质上都是在同一个线程里排队执行的。一个任务卡住了后面所有任务全等着。这个坑在第四部分会展开讲。3.4 execute 和 submit 也会被转成延迟任务这个细节可能在面试里不会被直接问但理解了之后会对定时线程池的整体设计有更清楚的认识。ScheduledThreadPoolExecutor 重写了 execute 方法当你调用 execute 或者 submit 提交任务时它并不会像普通线程池那样立即执行而是会被包装成延迟任务交给 DelayedWorkQueue延迟时间为 0 纳秒public void execute(Runnable command) { schedule(command, 0, TimeUnit.NANOSECONDS); }所以你在一个 ScheduledExecutorService 上调用 execute效果跟 schedule 一个延迟 0 秒的任务差不多任务会放进队列等空闲线程来取。这跟普通 ThreadPoolExecutor 的行为是有区别的如果你依赖 execute 之后任务立刻执行可能会踩到时序的坑。4. 实操中的避坑经验4.1 任务抛异常之后的“静默消失”这是定时任务场景里最隐蔽也最要命的问题我在线上遇到过多次。现象是周期任务跑着跑着突然不跑了日志里也没有任何异常堆栈就像任务凭空蒸发了一样。原因是任务内部抛了异常。ScheduledThreadPoolExecutor 执行任务时调用的是 runAndReset 方法这个方法会把任务状态重置以便下次继续跑但如果任务内部抛出异常异常会被 FutureTask 捕获并记录到任务对象里而周期任务又没人去调用 get() 方法获取结果异常信息就一直留在任务对象内部没有任何日志输出。更关键的是一旦任务抛了异常这个周期任务的生命周期就结束了下一次不会再触发。解决办法很简单任务内部必须做 try-catch 兜底千万不能让异常跑出去。// 正确写法把异常兜住 scheduler.scheduleAtFixedRate(() - { try { // 业务逻辑 doSomething(); } catch (Exception e) { // 至少要打日志不能吞掉 log.error(定时任务执行失败, e); } }, 0, 5, TimeUnit.SECONDS);尤其是对接外部系统的时候比如调第三方接口、读写数据库任何一次网络抖动、连接超时都可能导致异常。不包 try-catch任务就悄悄死掉而且是在没有任何告警的情况下。所以做定时任务的第一条军规就是异常必须兜住日志必须打全。4.2 长任务会拖垮整个线程池第二个常见的坑是线程数设置不合理。有人觉得定时任务嘛一个线程串着跑就行于是用 newSingleThreadScheduledExecutor或者把核心线程数设为 1结果任务稍微多一点就出问题。举个例子。你有一个每分钟执行一次的缓存刷新任务一个每小时执行一次的数据迁移任务共用一个单线程的 ScheduledExecutorService。数据迁移任务跑了一个小时期间缓存刷新任务一直排在队列里。因为线程池只有一个线程缓存任务只能眼巴巴等着。正常情况下缓存一分钟刷一次实际却变成了一个半小时刷一次缓存时效性大打折扣。更极端的场景是某个任务里做了阻塞操作比如调外部接口没设置超时时间线程卡在那里整个线程池就瘫痪了。所有依赖这个线程池的定时任务全部停摆而且因为任务还在执行中你连异常都看不到排查起来极其痛苦。所以线程数设置要根据任务实际情况来评估。我的经验是先用“同一时刻最多可能并行执行的周期任务数 1”作为核心线程数的起点再留点余量。比如有三个任务正常不会同时跑到设 2 个线程就够用如果有些任务执行时间可能很长建议把不同性质的任务拆到不同的线程池里避免互相影响。4.3 关闭线程池的正确姿势定时任务在应用重启或关闭时需要释放线程资源。直接不关是不行的因为 ScheduledThreadPoolExecutor 里的线程默认不是守护线程你不关的话JVM 根本退不出去进程会一直挂着。关闭时有两个方法shutdown() 和 shutdownNow()。shutdown() 会让线程池停止接受新任务等已提交任务全部执行完才真正关闭shutdownNow() 则是立刻中断正在执行的任务并返回队列中等待执行的任务列表。对定时任务来说我建议优先用 shutdown()因为强制中断可能让任务执行到一半留下脏数据。等待线程池关闭可以配合 awaitTermination 使用scheduler.shutdown(); try { if (!scheduler.awaitTermination(30, TimeUnit.SECONDS)) { // 30秒还没完全关闭强制关闭 scheduler.shutdownNow(); } } catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); }在 Spring Boot 项目中可以交给 JVM 的关闭钩子处理或者用 PreDestroy 注解在 Bean 销毁时执行关闭逻辑。总之这个习惯要养成否则本地调试时经常发现 IDE 停止程序后控制台还挂着。4.4 被取消的任务还在队列里“占坑”接着一个容易被忽略的点当你调用 future.cancel(true) 取消一个定时任务时如果这个任务已经被提交到 DelayedWorkQueue 里排队但还没到触发时间它并不会立刻从队列里移走。这个问题在周期长、任务多的情况下会被放大。比如你创建了 1000 个一次性延迟任务每个任务都设置了 1 小时的延迟后来业务变化你取消了一半。但如果队列里没有清理机制这 500 个已经取消的任务还留在队列里直到触发时间到才会发现已经被取消了再移除。队列中堆积大量已取消任务白白占用内存还拖慢队列操作。ScheduledThreadPoolExecutor 提供了一个开关来解决这个问题ScheduledThreadPoolExecutor scheduler new ScheduledThreadPoolExecutor(2); scheduler.setRemoveOnCancelPolicy(true);设置成 true取消任务时会立即从阻塞队列中移除。对于大量短周期、可能频繁取消的任务场景建议打开这个开关。Executors.newScheduledThreadPool 创建的线程池默认是 false需要自己转换成 ScheduledThreadPoolExecutor 类型来设置。4.5 不要在定时任务里“裸调”远程接口这一条算是经验之谈。很多人写定时任务直接在里面调用远程 API不设超时也不做降级。一旦对端服务变慢工作线程就会被拖住后面的任务全部积压。我有一个比较实用的处理思路如果定时任务里必须调用外部接口一定要给 HttpClient 设置连接超时和读取超时时间要显式指定不能依赖默认值。更稳妥的做法是把远程调用拆到独立的线程池里定时任务只负责发起调用并设定一个最大等待时间到点没回来就记日志等待下个周期再执行。还有一个细节任务里也不要轻易用 Thread.sleep() 来控制节奏。sleep 会白白占住线程池里宝贵的线程如果同时还有其他任务需要用线程它们就得排队。想要控制两次操作的间隔应该用 scheduleWithFixedDelay 或者干脆把“休息”交给调度器来管理而不是手工 sleep。4.6 Spring Scheduled 底层也是这一套很多业务项目里用的是 Spring 的 Scheduled 注解看起来跟 ScheduledExecutorService 没关系但它底层用的同样也是定时线程池的机制。Spring 的 TaskScheduler 默认实现是对 ScheduledExecutorService 的封装。这里有一个常见的坑如果没有显式配置线程池大小Spring 默认用单线程执行所有 Scheduled 任务。也就是说你写了 10 个 Scheduled 方法它们默认是在同一个线程里轮流执行的。一个任务跑太久其他任务全部排队。这个表现跟 newSingleThreadScheduledExecutor 的行为几乎一模一样因为底层就是同一套东西。解决办法是在配置文件中加上spring: task: scheduling: pool: size: 10或者手动定义一个 TaskScheduler Bean 并指定线程池大小。所以理解了 ScheduledExecutorServiceSpring 定时任务的很多问题也能直接看懂。5. 面试考点与高频问题整理5.1 几个高频面试题和回答方向面试官问这个知识点的时候通常集中在下面几个问题提前准备好现场就不会卡壳。第一个问题Timer 和 ScheduledExecutorService 有什么区别回答要点是两条Timer 单线程一个任务异常会拖垮全部任务ScheduledExecutorService 基于线程池实现支持多线程、并发执行、异常隔离还支持任务取消和更灵活的时间单位。第二个问题scheduleAtFixedRate 和 scheduleWithFixedDelay 有什么区别回答要点是前者按固定频率调度以前一次理论触发时间加周期计算下一次执行时间任务超时后可能补跑后者按固定延迟调度以上一次任务结束时间加延迟计算下一次执行时间任务超时后会顺延。第三个问题如果任务执行时间比周期还长会发生什么回答要点是fixed rate 模式下任务结束后立即执行下一次不会并发执行同一个任务fixed delay 模式下任务结束后再等满 delay 时间执行下一次。第四个问题ScheduledThreadPoolExecutor 的线程数怎么设为什么 maximumPoolSize 没意义回答要点是队列是无界 DelayedWorkQueue队列不会满所以不会创建非核心线程实际并发能力由 corePoolSize 决定按可能同时执行的任务数来设置。第五个问题定时任务抛了异常会继续执行吗回答要点是不会。runAndReset 会把异常记录在任务对象里但没人调用 get()异常被吞掉任务同时被取消后续不再执行。必须 try-catch 兜底。这些问题在 Java 面试中出现的频率很高而且很多是连环追问。把源码那套逻辑记熟之后基本能从使用层面聊到实现层面会比只会背答案的候选人强不少。5.2 技术选型什么时候用 JDK 自带什么时候上框架最后聊一下实际项目里的选型。JDK 自带的 ScheduledExecutorService 适合单机、任务量不大、调度逻辑简单的场景。比如几十个定时任务不需要持久化不需要跨节点保证只执行一次直接用这个就够了。如果项目里用的是 Spring Boot那优先考虑 Scheduled 注解因为它能直接跟 Spring 容器整合支持 cron 表达式写起来更简洁。但要注意默认单线程的问题配置好线程池大小。如果需求升级到分布式调度比如多个节点部署同一个定时任务只能在一个节点执行任务要支持失败重试、分片执行、动态调整执行时间那就得上 Quartz 或者 XXL-JOB 这类框架了。Quartz 支持集群模式Job 持久化到数据库但要自己处理分布式锁的问题XXL-JOB 自带调度中心和管理界面开箱即用。我的建议是别一上来就上重框架。简单任务用 JDK 自带或者 Spring 的 Scheduled 就很好系统的复杂度应该跟着业务需求走而不是跟着技术偏好走。另外关于分布式定时任务很多人的误区是没有调度中心就做不了分布式定时任务。其实如果你的部署节点就两三个用数据库行锁、Redis 分布式锁也能实现“全局唯一执行”的效果代价也不高。把底层这些执行语义搞清楚了再去看其他框架会发现它们底层也无非是在“到点该不该执行”“执行完了下次什么时候执行”这些问题上做文章。核心原理通了技术选型就变成水到渠成的事了。最后还是那句话定时任务看起来简单真正做稳了不容易。把异常兜好、线程数设置合理、关停流程做规范比写出花里胡哨的调度逻辑重要一百倍。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →