Java ScheduledExecutorService:从定时任务到异步调度的核心机制与实战
1. 从“定时”到“调度”为什么我们需要ScheduledExecutorService在Java开发里定时任务是个老生常谈的话题。早期我们可能用Thread.sleep()加个循环或者用Timer和TimerTask。但真正做过线上项目的朋友都知道这些老方案坑太多了Timer是单线程的一个任务执行慢了后面所有任务都得排队等着严重时直接导致任务堆积、内存泄漏自己写线程管理又得操心线程池创建、销毁、异常处理代码写出来又臭又长维护起来头皮发麻。所以当我们需要一个更可靠、更强大、更“企业级”的定时任务调度器时ScheduledExecutorService就登场了。它不是什么新潮玩意儿而是Java并发包java.util.concurrent里的一员老将但它的设计理念至今依然非常先进。简单说它把“任务调度”这个事儿从简单的“定时触发”升级到了“可管理、可控制、可扩展的异步执行体系”。你不再只是告诉程序“每隔5秒跑一次”而是能精细地控制这个任务在哪个线程池跑、任务堆积了怎么办、任务出异常了怎么处理、整个调度服务如何优雅地关闭。这背后的核心就是“异步”。ScheduledExecutorService并不自己执行任务它是个“调度指挥官”。它内部维护着一个延迟队列DelayedWorkQueue和一个线程池通常是ThreadPoolExecutor。指挥官负责按预设的时间比如3秒后或者每隔5秒把任务Runnable或Callable放进队列排队。线程池里的“工人线程”则异步地从队列里取出任务来执行。指挥和干活是两拨人互不阻塞。这就是它能避免Timer单线程阻塞问题的根本原因也是其“异步魔力”的起点。2. 核心机制拆解调度器如何实现精准与可靠要理解ScheduledExecutorService的魔力不能停留在API调用层面得看看它肚子里是怎么运转的。我们通常通过Executors.newScheduledThreadPool(int corePoolSize)来创建它返回的实际上是ScheduledThreadPoolExecutor这个类的实例。它的设计非常巧妙融合了线程池和优先级队列。2.1 心脏DelayedWorkQueue延迟队列这是调度功能的核心数据结构。它不是普通的LinkedBlockingQueue而是一个基于堆通常是二叉堆实现的优先级队列。队列里的每个元素ScheduledFutureTask都封装了我们提交的任务以及一个关键的属性下次执行的时间点time。堆顶的元素就是最近将要执行的那个任务。线程池的工作线程会不断地尝试从队列里取任务调用take()或poll()方法。DelayedWorkQueue的take()方法会判断堆顶任务的time是否已经到达或超过当前时间。如果没到工作线程就会在这个队列上等待awaitNanos(delay)精确的剩余延迟时间而不是傻等或忙等。这保证了任务能在理论上最精确的时间点被唤醒和执行实现了高效的资源利用。2.2 灵魂ScheduledFutureTask任务封装我们提交的Runnable或Callable会被包装成一个ScheduledFutureTask对象。这个对象是个“智能任务”它除了持有原始任务还额外记录了下次执行时间time根据调度类型一次性延迟、固定频率、固定延迟计算得出。执行周期period对于周期性任务这个值不为零。正数表示固定频率scheduleAtFixedRate负数表示固定延迟scheduleWithFixedDelay。序列号sequenceNumber当两个任务的time相同时用它来保证FIFO顺序避免不确定性。最关键的是它的run()方法。对于一次性任务执行完就结束了。对于周期性任务在执行完用户逻辑后它会自动计算下一次执行的时间点并把自己重新放入DelayedWorkQueue中等待下一次被调度。这个过程对用户是完全透明的这就是为什么我们调用scheduleAtFixedRate后就不用再管了的原因。2.3 大脑ScheduledThreadPoolExecutor调度逻辑这个类继承了ThreadPoolExecutor所以它具备所有标准线程池的能力核心线程数、最大线程数、拒绝策略等。但它重写了几个关键方法decorateTask用于将我们提交的Runnable/Callable包装成ScheduledFutureTask。delayedExecute这是提交任务后的核心入口。它不会直接执行任务而是先将任务ScheduledFutureTask插入DelayedWorkQueue。schedule、scheduleAtFixedRate、scheduleWithFixedDelay这些我们常用的方法内部都是先创建ScheduledFutureTask然后调用delayedExecute。这种设计实现了“调度”与“执行”的彻底解耦。调度器只负责管理任务队列和时间规划而具体的执行工作交给继承了ThreadPoolExecutor的“执行器”部分。这种架构让系统非常健壮即使某个任务执行时抛出了未捕获的异常默认会打印堆栈并丢弃也不会影响调度器线程和其他任务的正常调度除非异常导致线程退出且线程池没有足够的线程补充。3. 三种调度模式详解不只是“每隔X秒”很多人只知道scheduleAtFixedRate但其实ScheduledExecutorService提供了三种核心模式用错了场景效果天差地别。3.1 一次性延迟执行schedule这是最简单的一种。比如用户下单后如果30分钟内未支付系统自动取消订单。ScheduledExecutorService scheduler Executors.newScheduledThreadPool(2); ScheduledFuture? future scheduler.schedule(() - { // 检查订单状态若未支付则取消 cancelOrder(orderId); }, 30, TimeUnit.MINUTES); // 如果用户中途支付了可以取消这个定时任务 // future.cancel(true);关键点它返回一个ScheduledFuture这个对象非常有用。你可以用它来查询任务是否完成isDone或者在最晚执行前取消任务cancel。这在实现“可取消的延迟操作”时是必备的。3.2 固定频率执行scheduleAtFixedRate这是最容易被误解的模式。它的定义是以固定的频率执行任务无论上次任务是否完成。假设你每隔5秒执行一次任务那么任务的初始执行时间记为T后续的执行时间点理论上是T5s, T10s, T15s... 它关注的是“开始执行的频率”。// 每5秒尝试发送一次心跳包不关心上一次发送是否成功或耗时 scheduler.scheduleAtFixedRate(() - { sendHeartbeat(); }, 0, 5, TimeUnit.SECONDS);坑点与应对如果任务的执行时间超过了周期比如任务要跑8秒周期是5秒会发生什么调度器不会开新线程来并行执行同一个任务那是scheduleWithFixedDelay的逻辑吗不也不是。对于scheduleAtFixedRate如果上次任务还没跑完到了下一个周期点新任务会被提交到队列里等待但不会立即执行。它会等当前正在执行的任务完成后由同一个或另一个工作线程立刻执行这个被延迟的任务然后后续的调度时间会“追赶”。这可能导致任务堆积。所以scheduleAtFixedRate只适用于执行时间非常稳定且远小于周期的任务比如心跳、定期轻量级数据采样。3.3 固定延迟执行scheduleWithFixedDelay这是更常用、也更符合直觉的周期性任务模式。它的定义是在一次任务执行结束之后延迟固定的间隔再开始下一次任务。它关注的是“任务结束到下一次开始的间隔”。// 每隔一段时间清理一次临时文件必须等上次清理完成后再等10分钟 scheduler.scheduleWithFixedDelay(() - { cleanTempFiles(); // 假设这个操作耗时不确定 }, 0, 10, TimeUnit.MINUTES);核心区别假设cleanTempFiles()这次花了2分钟那么下次执行不是在10分钟后而是在这次执行结束后的10分钟也就是12分钟后。这保证了任务之间总有至少10分钟的间隔避免了任务重叠执行和潜在的资源竞争。对于执行时间不确定、或者任务本身要求必须串行执行不能重叠的场景必须使用scheduleWithFixedDelay。选择口诀要“准点发车”用Rate固定频率要“干完歇会儿”用Delay固定延迟。不确定耗时或怕冲突无脑选Delay更安全。4. 进阶实战避坑指南与性能调优会用API只是第一步想在生产环境玩转ScheduledExecutorService下面这些坑你得一个个趟过去。4.1 优雅关闭shutdown与shutdownNow的天壤之别这是面试高频题也是线上事故高发区。很多人程序退出时不管不顾导致任务数据丢失。shutdown()温和关闭。调用后调度器不再接受新任务但会继续执行队列里已存在的包括已到点和未到点的定时任务。所有任务完成后线程池才真正关闭。shutdownNow()立即关闭。调用后尝试中断所有正在执行的任务并清空任务队列返回那些尚未开始执行的任务列表。注意它试图中断线程但如果你的任务没有响应中断比如没有检查Thread.interrupted()状态或处理InterruptedException任务可能停不下来。对于定时任务绝大多数情况你应该使用shutdown()并结合awaitTermination方法。scheduler.shutdown(); // 发起关闭 try { // 等待现有任务执行完毕最多等1小时 if (!scheduler.awaitTermination(1, TimeUnit.HOURS)) { // 如果超时后还有任务没完可以考虑强制关闭 ListRunnable droppedTasks scheduler.shutdownNow(); log.warn(Scheduler did not terminate in time, dropped {} tasks., droppedTasks.size()); } } catch (InterruptedException e) { // 如果等待过程被中断也立即强制关闭 scheduler.shutdownNow(); Thread.currentThread().interrupt(); // 保持中断状态 }关键经验在你的任务代码里一定要考虑中断响应。尤其是在循环或阻塞调用如Thread.sleep,Object.wait,BlockingQueue.take中要捕获InterruptedException并妥善处理通常是在清理资源后退出。4.2 异常处理沉默的杀手这是ScheduledExecutorService最大的一个“坑”。如果你提交的任务是Runnable并且在执行中抛出了未捕获的异常RuntimeException或Error会发生什么**默认情况下这个异常会被任务所在的线程的UncaughtExceptionHandler处理而ScheduledThreadPoolExecutor默认的处理器可能只是简单地打印一下堆栈跟踪到标准错误流然后这个任务就静默地失败了。更糟糕的是对于周期性任务scheduleAtFixedRate或scheduleWithFixedDelay如果某次执行抛出未捕获异常整个周期性调度就会停止后续的调度不会再进行而且没有任何明显的错误日志除非你盯着标准错误输出。解决方案使用Callable替代RunnableCallable允许通过Future.get()获取执行结果或异常。但对于周期性任务这招不灵。在任务内部进行最彻底的try-catch这是最可靠的方法。scheduler.scheduleAtFixedRate(() - { try { doBusinessLogic(); } catch (Throwable t) { // 抓住所有Throwable log.error(Scheduled task failed, but will continue next cycle., t); // 这里可以进行告警、指标上报等 } }, 1, 10, TimeUnit.SECONDS);自定义线程工厂为线程设置全局的UncaughtExceptionHandler这样可以捕获所有未处理的异常但依然无法阻止周期性任务因异常而终止的问题。它更适合做最后的日志收集和告警。4.3 核心线程数配置与任务隔离创建调度线程池时你需要指定corePoolSize。这个参数在ScheduledThreadPoolExecutor里有特殊行为即使没有任务核心线程也会一直存活不会被回收。这是为了确保定时任务能准时被调度。如何设置大小这取决于你的任务类型。如果都是短时、低频任务核心线程数可以设小一点比如2-4。如果任务执行时间较长如超过100ms或者任务数量很多你需要增加核心线程数以避免任务堆积在队列里导致后续任务被延迟。一个重要的原则是隔离不要把不相关的、重要性不同的定时任务扔进同一个调度器。比如一个每10毫秒执行的高频数据采集任务和一个每小时执行一次的重量级报表生成任务放在一起就不合适。高频任务可能会因为报表任务的长时间执行而被严重延迟。最好按业务域或性能要求创建多个ScheduledExecutorService实例。4.4 内存泄漏陷阱持有外部引用定时任务经常会持有外部对象的引用例如某个Service实例、数据库连接池等。如果这个定时任务一直存在于调度队列中比如一个周期很长的任务而外部对象本应被垃圾回收但由于被定时任务引用而无法回收就会导致内存泄漏。典型场景在Web应用中你为一个用户会话HttpSession相关的对象创建了一个清理任务并提交到了全局的ScheduledExecutorService。用户退出后会话对象因为被这个定时任务引用而无法释放。解决方案使用弱引用WeakReference来持有外部对象。这样当外部对象没有其他强引用时可以被GC回收你的定时任务在下次执行时通过弱引用获取到的对象就是null此时任务可以自动判断并取消自己。public class CleanupTask implements Runnable { private final WeakReferenceSomeResource resourceRef; private final ScheduledFuture? future; public CleanupTask(SomeResource resource, ScheduledFuture? future) { this.resourceRef new WeakReference(resource); this.future future; } Override public void run() { SomeResource resource resourceRef.get(); if (resource null) { // 资源已被GC取消定时任务 future.cancel(false); log.info(Resource garbage collected, task cancelled.); return; } // 正常执行清理逻辑 resource.cleanup(); } }5. 超越原生与Spring及分布式调度框架的协作在Spring Boot等现代框架中我们很少直接裸用ScheduledExecutorService而是用Scheduled注解。但了解底层原理能帮你更好地使用和排查问题。5.1 Spring Scheduled的底层Spring的定时任务调度默认也是基于ScheduledExecutorService实现的。当你使用EnableScheduling时Spring会默认注册一个ThreadPoolTaskScheduler它内部包装了一个ScheduledExecutorService。Scheduled注解支持的fixedRate和fixedDelay属性分别对应我们上面讲的两种模式。Spring的优势在于集成它可以很方便地注入Spring管理的Bean享受AOP如事务、日志等能力。但坑也是一样的任务抛异常会导致周期性任务停止。所以在Spring定时任务方法里进行完整的try-catch同样至关重要。5.2 何时需要分布式任务调度ScheduledExecutorService是单机版的调度器。在微服务或集群部署时如果你在每台机器上都启动了同样的定时任务会导致任务被重复执行比如每天凌晨的全局数据统计会跑多次。这时你就需要引入分布式任务调度框架如XXL-JOB、Elastic-Job、Quartz Cluster等。这些框架的核心思想是引入一个“调度中心”由它来统一管理所有任务的触发时间并通过选举机制每次只将任务下发到集群中的某一个节点执行。那么ScheduledExecutorService在分布式架构中就无用武之地了吗并非如此。它非常适合处理节点内部的、轻量级的、对实时性要求高的调度需求。例如缓存刷新每5分钟刷新一次本地Guava Cache。连接保活每30秒向连接池中的空闲连接发送一个ping。监控采样每2秒采集一次本机的JVM指标。异步任务重试实现一个简单的异步重试机制失败后延迟一段时间再试。这些任务不需要全局唯一每个节点自己管理自己的那份就行用ScheduledExecutorService简单高效。5.3 实现一个简单的异步任务重试器利用ScheduledExecutorService的延迟执行特性我们可以很容易地实现一个任务重试机制。这比简单的Thread.sleep更优雅因为它不阻塞当前线程并且可以方便地管理重试次数和间隔。public class AsyncRetryExecutor { private final ScheduledExecutorService scheduler Executors.newScheduledThreadPool(2); public T void executeWithRetry(CallableT task, int maxRetries, long delay, TimeUnit unit) { executeWithRetryInternal(task, maxRetries, delay, unit, 0); } private T void executeWithRetryInternal(CallableT task, int maxRetries, long delay, TimeUnit unit, int currentRetry) { scheduler.schedule(() - { try { T result task.call(); log.info(Task succeeded on retry {}. Result: {}, currentRetry, result); } catch (Exception e) { log.warn(Task failed on retry {}., currentRetry, e); if (currentRetry maxRetries) { log.info(Scheduling retry {} after {} {}, currentRetry 1, delay, unit); // 递归调用安排下一次重试 executeWithRetryInternal(task, maxRetries, delay, unit, currentRetry 1); } else { log.error(Task failed after {} retries. Giving up., maxRetries); } } }, currentRetry 0 ? 0 : delay, unit); // 第一次立即执行后续延迟执行 } public void shutdown() { scheduler.shutdown(); } }这个例子展示了如何将调度器用于业务逻辑而不仅仅是简单的定时触发。通过组合调度与递归可以构建出更复杂的异步工作流。ScheduledExecutorService的“异步魔力”本质在于它将时间管理与任务执行解耦通过精妙的队列和线程池设计提供了可靠、灵活、易用的调度能力。理解其核心机制能让你在遇到任务不执行、执行不准时、CPU飙升、内存泄漏等问题时快速定位根因。而掌握其进阶用法和避坑技巧则能让你在设计和实现后台任务系统时更加游刃有余写出既高效又健壮的代码。它可能不是最炫酷的技术但绝对是Java开发者工具箱里最值得信赖的实用工具之一。

相关新闻

AI Agent记忆系统设计:从长上下文到向量检索的工程实践

AI Agent记忆系统设计:从长上下文到向量检索的工程实践

1. 从“长上下文”到“好记忆”的认知鸿沟最近在折腾各种AI Agent项目时,一个现象让我感触颇深:很多开发者,包括我自己在早期,都陷入了一个技术“舒适区”的误区——认为只要给模型喂足够长的上下文,它就能记住所有事情…

2026/10/13 6:43:44 阅读更多 →
线上课程用户行为数据分析:从指标体系到深度洞察的实践指南

线上课程用户行为数据分析:从指标体系到深度洞察的实践指南

1. 项目概述:从数据中“听见”课堂的回声做线上教育平台的朋友,最近是不是总被老板或投资人问:“我们的课程到底学得怎么样?用户为什么来,又为什么走?” 手里握着海量的用户点击、观看、互动数据&#xff0…

2026/10/5 23:08:45 阅读更多 →
Sysinternals 2020年更新深度解析:从持久化检测到MOTW取证的Windows安全工具箱实战

Sysinternals 2020年更新深度解析:从持久化检测到MOTW取证的Windows安全工具箱实战

1. 项目概述:一次迟到的“工具箱”深度体检作为一名常年与Windows系统打交道的运维工程师和安全研究员,我对Sysinternals这套工具的感情,大概就像一位老木匠对他的工具箱——熟悉、信赖,且深知每一件工具的脾气。2020年6月的那次更…

2026/9/26 3:25:57 阅读更多 →

最新新闻

连接器可靠性试验:振动、冲击、碰撞三者的区别与工程选择

连接器可靠性试验:振动、冲击、碰撞三者的区别与工程选择

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 6:42:49 阅读更多 →
AnyPS5跨平台串流实战:主机编码、网络传输与客户端解码全解析

AnyPS5跨平台串流实战:主机编码、网络传输与客户端解码全解析

1. 从“AnyPS5”这个标题说起:一个跨平台串流工具的设计野心第一次看到“AnyPS5”这个命名,我的直觉是:这大概率是一个围绕主机游戏串流展开的项目,而且“Any”这个前缀暗示了它的核心卖点——跨平台、跨设备、尽可能抹平硬件壁垒…

2026/10/13 6:42:49 阅读更多 →
谱减法语音增强实战:从原理到参数调优的完整指南

谱减法语音增强实战:从原理到参数调优的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 6:42:49 阅读更多 →
PS5兼容性研究实战:状态识别、工具链匹配与恢复兜底

PS5兼容性研究实战:状态识别、工具链匹配与恢复兜底

我知道很多人看到“AnyPS5”这个名字,第一反应八成是“是不是所有PS5都能折腾了”。但真正在软硬件研究这个圈子里待过一段时间你就明白,“能否折腾”只是结果,背后真正值钱的是兼容性研究那一整套逻辑。不同型号的主板布局不一样&#xff0c…

2026/10/13 6:42:49 阅读更多 →
Windows下PyCharm连接Anaconda:虚拟环境与解释器配置全攻略

Windows下PyCharm连接Anaconda:虚拟环境与解释器配置全攻略

简介:Windows环境下在PyCharm中配置Anaconda开发环境,是不少Python初学者和数据科学入门者常遇到的痛点。这份超详细教程面向上述读者,清晰梳理了Python、PyCharm与Anaconda三者各自的角色与协作关系,并针对安装、导入、包管理过程…

2026/10/13 6:42:49 阅读更多 →
FPGA入门必做:可逆计数器设计详解与上板调试

FPGA入门必做:可逆计数器设计详解与上板调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 6:41:49 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →