深入AQS:从设计原理到手写同步器与线上问题排查
1. 为什么AQS是Java并发里绕不开的那道坎刚入行那会儿我对ReentrantLock、CountDownLatch、Semaphore这些工具类的态度就是“会用就行”直到有一次线上一个定时任务莫名其妙卡死线程堆栈打出来一看一堆线程停在LockSupport.park上我才意识到——不把AQSAbstractQueuedSynchronizer队列同步器搞明白Java并发这块永远是黑盒。AQS是整个java.util.concurrent包的基石ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、CyclicBarrier底层用ReentrantLock、ThreadPoolExecutor里的Worker加锁全都建立在它之上。你不需要背下它每一行源码但你必须理解它解决问题的思路用一个volatile int state表示同步状态用一个CLH变体的双向队列管理等待线程用CAS保证状态变更的原子性用LockSupport.park/unpark做线程阻塞与唤醒。这套设计把“怎么抢资源”和“抢不到怎么办”这两件事彻底解耦子类只需要关心state的语义排队、阻塞、唤醒这些脏活累活AQS全包了。这篇文章我会从设计动机讲到源码细节再讲到实际排查问题的经验适合已经会写并发代码但想搞清楚底层原理的开发者也适合准备面试想把这部分讲透的同学。看完你至少能做到自己写一个基于AQS的同步器看懂ReentrantLock的公平与非公平差异遇到线程卡死能快速定位是不是AQS队列出了问题。2. AQS的整体设计思路拆解2.1 一个state加一条队列解决所有同步问题AQS的核心抽象非常简洁就两个东西一个是volatile int state一个是内部类Node构成的双向链表队列。state的含义完全由子类定义——ReentrantLock里它表示重入次数Semaphore里它表示可用许可数CountDownLatch里它表示还没完成的计数。这种设计叫“模板方法模式”AQS把同步器的骨架搭好把state的获取和释放逻辑留给子类通过tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared、isHeldExclusively这几个方法去实现。为什么用int而不是boolean或者long因为int足够表达大多数场景的语义重入次数、许可数量、计数都能覆盖而且int的CAS操作在各平台都有良好支持。为什么用volatile保证state的可见性一个线程修改了state其他线程能立刻看到。但volatile本身不保证原子性所以修改state必须配合CAS这就是AQS里大量出现compareAndSetState的原因。队列的设计更值得说道。AQS内部维护的是一个CLHCraig, Landin, and Hagersten锁队列的变体。原版CLH队列是自旋锁队列每个线程在自己的前驱节点上自旋等待。AQS把它改造成了阻塞队列——每个节点代表一个等待线程节点里有waitStatus表示线程状态CANCELLED、SIGNAL、CONDITION、PROPAGATE有prev和next指针构成双向链表。为什么改成双向因为取消节点时需要从队列中摘除单向链表做不到。为什么用阻塞而不是自旋自旋在竞争激烈时白白消耗CPU阻塞让出CPU给其他线程更划算。注意AQS的队列是一个“虚拟”队列head节点本身不存储等待线程或者说head代表当前持有锁的线程真正等待的线程从head.next开始。理解这一点对看懂acquire和release的源码至关重要。2.2 独占模式与共享模式的分野AQS支持两种同步模式独占模式Exclusive和共享模式Shared。独占模式一次只允许一个线程持有同步状态比如ReentrantLock共享模式允许多个线程同时持有比如Semaphore和CountDownLatch。这两种模式在AQS里对应两套方法独占模式用acquire/release共享模式用acquireShared/releaseShared。它们的核心区别在于独占模式下一个线程拿到state后其他线程必须等待共享模式下只要state还有剩余后续线程可以继续获取。体现在队列上独占模式释放时会唤醒后继节点共享模式释放时会传播唤醒PROPAGATE状态就是为此而生。为什么要分两种模式而不是统一用一种因为语义完全不同。独占锁的释放只需要唤醒一个后继共享锁的释放可能需要唤醒多个。如果强行统一要么共享场景下唤醒不足导致线程饥饿要么独占场景下过度唤醒造成不必要的竞争。分开设计让每种模式的逻辑都足够清晰。2.3 公平与非公平一场关于吞吐量和延迟的权衡ReentrantLock可以构造为公平锁或非公平锁这个选择直接体现在AQS的tryAcquire实现上。公平锁的tryAcquire会先检查队列里有没有前驱节点如果有就老老实实排队非公平锁则直接尝试CAS抢state抢到了就插队执行。非公平锁的吞吐量通常更高因为减少了线程上下文切换——刚释放锁的线程很可能立刻再次获取锁如果让它插队就省去了一次唤醒等待线程的开销。但非公平锁可能导致队列中的线程长时间得不到执行也就是“饥饿”。公平锁保证了FIFO顺序延迟更可预测但吞吐量会下降。实际选型时绝大多数场景用非公平锁就够了ReentrantLock默认也是非公平的。只有在对公平性有严格要求、且竞争不激烈的场景下才考虑公平锁。我个人的经验是如果你不确定该用哪个用非公平的性能更好而且大多数业务场景对那点延迟差异并不敏感。3. AQS核心源码细节与实操要点3.1 Node节点的waitStatus到底怎么理解Node里的waitStatus是AQS里最容易让人迷糊的地方它有五个取值每个都有特定含义状态值名称含义1CANCELLED节点已取消线程因超时或中断放弃等待-1SIGNAL后继节点需要被唤醒当前节点释放锁时要unpark后继-2CONDITION节点在Condition队列中等待-3PROPAGATE共享模式下释放需要传播唤醒0初始状态新节点入队时的默认值关键要理解SIGNAL当一个节点把前驱的waitStatus设为SIGNAL时意思是“前驱你释放锁的时候记得叫醒我”。所以shouldParkAfterFailedAcquire方法里如果前驱状态是SIGNAL就返回true当前线程可以安心park如果前驱状态是CANCELLED就往前找有效前驱否则用CAS把前驱状态设为SIGNAL再重试。实操心得调试AQS相关问题时打印队列里每个节点的waitStatus能快速判断队列是否健康。如果看到大量CANCELLED节点堆积说明有线程频繁超时或中断可能需要检查业务逻辑里的超时设置是否合理。3.2 acquire方法的完整执行链路acquire(int arg)是独占模式获取锁的入口它的逻辑可以拆成三步public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }第一步调用子类实现的tryAcquire尝试获取。成功就直接返回整个方法结束连队列都不用碰。这是AQS的快路径设计——无竞争时性能极高。第二步tryAcquire失败调用addWaiter把当前线程包装成Node加入队列尾部。入队用CAS操作tail指针保证并发安全。如果CAS失败说明有其他线程也在入队就进入enq方法用自旋CAS的方式完成入队。第三步acquireQueued让节点在队列中等待。这个方法是个循环每次循环先检查前驱是不是head如果是就再试一次tryAcquire因为head释放锁了有机会了。如果获取成功把当前节点设为head并返回。如果失败判断是否需要park需要就调用LockSupport.park阻塞醒来后继续循环。整个链路的设计哲学是能抢就抢抢不到就排队排队时该阻塞就阻塞被唤醒后继续抢。每一步都有明确的退出条件不会无限自旋。3.3 release方法如何唤醒后继节点release(int arg)的逻辑相对简单public final boolean release(int arg) { if (tryRelease(arg)) { Node h head; if (h ! null h.waitStatus ! 0) unparkSuccessor(h); return true; } return false; }先调用子类的tryRelease只有完全释放比如重入次数减到0才返回true。然后检查head节点如果head不为空且waitStatus不为0就调用unparkSuccessor唤醒后继。unparkSuccessor里有个细节如果head.next是null或者状态是CANCELLED就从tail往前遍历找到最靠近head的有效节点。为什么从后往前找因为入队时是先设置node.prev再CAS设置tail所以从tail往前遍历一定能遍历到所有已入队的节点而从head往后可能因为next指针还没设置而漏掉节点。注意unparkSuccessor唤醒的是队列里第一个有效节点但被唤醒的线程不一定能立刻拿到锁它还要和其他新来的线程竞争。这就是非公平锁的体现。3.4 共享模式的传播机制共享模式的releaseShared和acquireShared比独占模式多了一个传播逻辑。当tryReleaseShared返回true时会调用doReleaseShared这个方法里有个循环不断检查head的waitStatus如果是SIGNAL就CAS设为0并唤醒后继如果是0就CAS设为PROPAGATE。PROPAGATE状态的作用是确保共享模式下唤醒能正确传播。考虑一个场景多个线程同时释放共享锁如果不加PROPAGATE可能出现唤醒丢失——一个线程释放后唤醒了后继但后继还没来得及执行另一个线程又释放了此时head状态已经变了可能导致后续等待线程无法被唤醒。PROPAGATE就是用来标记“还有唤醒需要传播”。这个机制比较绕实际开发中很少需要直接操作但理解它有助于排查共享锁相关的诡异问题。4. 手写一个基于AQS的同步器4.1 需求定义实现一个不可重入的互斥锁光看源码不够自己写一个才能真懂。我们来实现一个最简单的不可重入互斥锁state为0表示未锁定为1表示已锁定。public class SimpleMutex implements Lock { private static class Sync extends AbstractQueuedSynchronizer { Override protected boolean tryAcquire(int arg) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } Override protected boolean tryRelease(int arg) { if (getState() 0) { throw new IllegalMonitorStateException(); } setExclusiveOwnerThread(null); setState(0); return true; } Override protected boolean isHeldExclusively() { return getState() 1; } Condition newCondition() { return new ConditionObject(); } } private final Sync sync new Sync(); Override public void lock() { sync.acquire(1); } Override public void unlock() { sync.release(1); } Override public boolean tryLock() { return sync.tryAcquire(1); } Override public Condition newCondition() { return sync.newCondition(); } // 其他Lock接口方法省略 }这个实现里tryAcquire用CAS把state从0改成1成功就记录当前线程为持有者。tryRelease把state设回0并清除持有者。注意tryRelease里没有用CAS因为释放锁的线程一定是持有锁的线程不存在竞争。4.2 关键点为什么释放时不需要CAS很多初学者会问为什么tryAcquire用CAS而tryRelease直接set因为获取锁时可能有多个线程同时竞争必须用CAS保证只有一个成功而释放锁时只有持有锁的线程才会调用release其他线程根本没资格释放所以不存在并发问题。这是AQS设计里“获取要防并发释放不用防”的典型体现。但有个例外共享模式下释放也可能有并发因为多个线程可能同时持有共享状态所以tryReleaseShared通常需要用CAS循环。比如Semaphore的释放就是CAS增加state。4.3 测试验证多线程竞争下的正确性写个测试验证一下public class MutexTest { private static int counter 0; private static final SimpleMutex mutex new SimpleMutex(); public static void main(String[] args) throws InterruptedException { Thread[] threads new Thread[10]; for (int i 0; i 10; i) { threads[i] new Thread(() - { for (int j 0; j 10000; j) { mutex.lock(); try { counter; } finally { mutex.unlock(); } } }); threads[i].start(); } for (Thread t : threads) { t.join(); } System.out.println(counter counter); } }如果输出100000说明互斥锁工作正常。如果小于100000说明有并发问题。这个测试虽然简单但能验证AQS的基本功能。实操心得自己写同步器时最容易犯的错误是忘记在tryAcquire里调用setExclusiveOwnerThread导致isHeldExclusively判断错误。另外tryRelease里一定要检查当前线程是否是持有者否则可能被其他线程误释放。5. 常见问题与排查技巧实录5.1 线程卡在park上不动了怎么办这是AQS相关最常见的线上问题。现象是线程堆栈显示大量线程停在LockSupport.park业务逻辑不推进。排查思路分三步第一步确认是哪个同步器导致的。看堆栈里park的上层调用是ReentrantLock.lock还是Semaphore.acquire还是别的。不同同步器的语义不同排查方向也不同。第二步检查是否有线程持有锁但一直不释放。用jstack找到持有锁的线程看它在干什么。常见原因是持有锁的线程在执行耗时操作比如IO、远程调用时没有设置超时或者死锁了。第三步检查AQS队列状态。如果能看到队列里有大量CANCELLED节点说明很多线程等待超时或被中断了可能是锁的持有时间过长。如果队列正常但head一直不释放那就是持有锁的线程出了问题。现象可能原因排查方法大量线程park锁持有时间过长检查持有锁线程的堆栈队列大量CANCELLED超时设置过短调整tryLock超时时间线程永远不醒唤醒丢失检查release逻辑是否正确公平锁下吞吐骤降频繁上下文切换考虑改用非公平锁5.2 为什么我的Condition.await不生效Condition是AQS提供的等待/通知机制用的时候有几个坑第一个坑await必须在持有锁的情况下调用否则抛IllegalMonitorStateException。因为Condition内部会检查当前线程是否持有锁。第二个坑await会释放锁并进入Condition队列被signal唤醒后会从Condition队列转移到AQS同步队列重新竞争锁。这个过程涉及两次队列操作如果理解不清容易误判。第三个坑signal只是把节点从Condition队列转移到同步队列并不会立刻唤醒线程。真正唤醒发生在同步队列里前驱释放锁的时候。所以signal之后调用unlock是必须的否则被唤醒的线程拿不到锁。注意Condition的await和Object.wait语义类似但有本质区别——await可以有多个等待队列每个Condition一个而wait只有一个。这让Condition能实现更精细的线程协作。5.3 重入锁的重入次数溢出问题ReentrantLock的state是int类型重入次数理论上限是Integer.MAX_VALUE。正常业务不可能重入这么多次但如果代码里有递归加锁且递归深度失控可能触发溢出。溢出后state变成负数tryAcquire的判断逻辑会出错。排查这种问题看堆栈里是否有异常深的递归调用或者用反射读取state值看是否异常。预防措施是避免在递归里加锁或者用tryLock设置超时。5.4 共享模式下唤醒丢失的诡异现象共享模式比独占模式更容易出问题因为唤醒需要传播。典型现象是明明有线程释放了共享锁但等待的线程就是不醒。这通常是因为doReleaseShared里的CAS操作失败后没有重试或者PROPAGATE状态处理不当。排查时重点看doReleaseShared的执行路径确认每次释放都正确设置了head的waitStatus。如果用的是自己实现的共享同步器检查tryReleaseShared是否返回了正确的布尔值——返回true才会触发唤醒传播。6. 从AQS延伸出去的设计启示6.1 模板方法模式在框架设计中的威力AQS是模板方法模式的教科书级应用。它把不变的部分队列管理、阻塞唤醒、状态CAS放在基类把变化的部分state语义留给子类。这种设计让新增一个同步器变得极其简单——你只需要实现几个try方法剩下的AQS全帮你搞定。这种思路在实际开发中很值得借鉴。比如你做的是一个任务调度框架可以把任务排队、线程管理、超时控制这些通用逻辑放在基类把任务的具体执行逻辑留给子类。这样新增任务类型时不用重复造轮子。6.2 无锁编程的边界在哪里AQS大量使用CAS但并不是所有地方都用无锁。入队操作在CAS失败时会退化成自旋park/unpark本身是重量级操作。这说明无锁不是银弹AQS的设计者很清楚这一点——快路径用CAS慢路径用锁和阻塞两者结合才能兼顾性能和正确性。实际开发中不要盲目追求无锁。CAS适合竞争不激烈的场景竞争激烈时自旋开销可能比锁还大。判断标准很简单如果CAS失败率超过一定阈值比如30%就该考虑用锁了。6.3 读懂AQS对日常开发的直接价值很多人觉得AQS离业务开发很远其实不然。你用的线程池、你写的并发工具、你排查的线上问题背后都可能是AQS。理解AQS之后你看ReentrantLock的文档能看懂公平性参数的含义看Semaphore能理解许可的获取释放逻辑看线程池能明白Worker为什么要用AQS而不是synchronized。更重要的是AQS教会你一种思维方式把复杂问题拆解成状态管理和排队等待两个独立问题分别用最合适的机制解决。这种思维方式比具体的API用法有价值得多。我在实际项目里踩过最深的坑是一个基于ReentrantLock的定时任务因为持有锁期间调用了外部服务外部服务超时导致锁一直不释放最终整个线程池被拖垮。后来改成tryLock加超时问题才解决。这件事让我明白AQS给了你强大的工具但怎么用、用多久、什么时候放弃还是得靠开发者自己判断。工具再好也替代不了对业务场景的思考。

相关新闻

正整数划分经典解法:从动态规划到完全背包的笔试题实战

正整数划分经典解法:从动态规划到完全背包的笔试题实战

写笔试题有个很有意思的现象:越是看起来简单的数学题,越容易把人卡在笔试现场。正整数划分就是典型代表。题目往往只有一句话:“把一个正整数拆成若干个正整数之和,求方案数。”看完题目觉得不难,真到动手写状态转移方…

2026/10/16 12:02:04 阅读更多 →
C# .NetCore Swagger ISchemaFilter 应用场景

C# .NetCore Swagger ISchemaFilter 应用场景

目录 核心定义与用途 实现与注册步骤 常见应用场景 注意事项 如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。 ISchemaFilter 是 Swashbuckle (Swagger for ASP.NET Core) 提供的接口,用于在生成 OpenAPI 文档时…

2026/10/16 12:01:03 阅读更多 →
120-杨逢昌:三级检查节拍表交接时段优化(基于归位三定闭环法定频机制)

120-杨逢昌:三级检查节拍表交接时段优化(基于归位三定闭环法定频机制)

《6S管理实战专栏》 三环实战篇(第120篇) 杨逢昌使命: 用6S的力量,让10万名朋友实现高效愉悦的生活与工作。文本导读:多数工厂三级检查体系存在交接时段管控盲区,形成 6S 管理空档漏洞。如何消除漏洞呢&…

2026/10/16 12:01:03 阅读更多 →

最新新闻

Doris与Hadoop生态融合实践:Hive外表、HDFS分层与批流协同

Doris与Hadoop生态融合实践:Hive外表、HDFS分层与批流协同

大数据圈里有个很有意思的现象:你问一个团队为什么还在维护一套陈旧的大数据平台,一半人会回答“业务跑得好好的,没人敢动”;可你要是问他们最头疼什么,大概率是那句“报表越跑越慢,数仓越修越胖”。我这两…

2026/10/16 12:44:23 阅读更多 →
阿里云短信接口开发实战:从Spring Boot集成到踩坑优化

阿里云短信接口开发实战:从Spring Boot集成到踩坑优化

简介:阿里云短信接口开发示例压缩包,面向需要快速对接短信服务的后端开发者,适用于用户验证码、通知推送等场景。内容围绕阿里云短信服务的完整接入流程,从账号注册、创建短信服务实例、申请AccessKey密钥,到阅读官方S…

2026/10/16 12:44:23 阅读更多 →
sqli-labs Less 1 新手教程:SQL注入从判断注入点到联合查询脱库

sqli-labs Less 1 新手教程:SQL注入从判断注入点到联合查询脱库

sqli-labs 是我见过最适合新手入门 SQL 注入的靶场,没有之一。Less 1 又是整个靶场的第一关,难度最低,但它把 SQL 注入最核心的“判断注入点、确认列数、找显示位、联合查询脱库”这条完整链路都串起来了。我第一次接触 SQL 注入就是从这一关…

2026/10/16 12:44:23 阅读更多 →
LNTON烟火识别本地部署全指南:CUDA/cuDNN/Python环境避坑实战

LNTON烟火识别本地部署全指南:CUDA/cuDNN/Python环境避坑实战

简介:本资源是面向安防监控、智能视觉开发及边缘AI部署工程师的烟火与烟雾识别实用工具包,基于LNTON羚通算法实现高精度实时检测,适用于森林防火、工厂安全巡检、仓储物流等高风险场景的早期预警需求。压缩包共874个文件,含843张标…

2026/10/16 12:44:23 阅读更多 →
淘宝item_get接口调用全攻略:从签名机制到高频错误排查

淘宝item_get接口调用全攻略:从签名机制到高频错误排查

做电商开发的,没人能绕开商品详情这个接口。不管你是做ERP、做比价工具,还是做数据分析,只要有商品信息同步的需求,最后都会撞到同一个地方:淘宝开放平台的item_get。这个接口我前前后后用了几年,踩过坑也填…

2026/10/16 12:44:23 阅读更多 →
OpenCV图像去噪原理与工业级实战指南

OpenCV图像去噪原理与工业级实战指南

1. 为什么一张图的噪点处理,真能绕开PS?“从去除噪点的说起,有OpenCV要什么PS?”——这句话刚在技术群被转发时,我正帮某高校实验室处理一批显微成像数据。他们用的是一台老款CMOS传感器相机,拍出来的细胞切…

2026/10/16 12:43:19 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

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

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练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/15 9:49:14 阅读更多 →