Java无锁编程核心:CAS原子类与ABA问题实战解析
几乎每个从初级走向高级的Java工程师都会在某一刻突然意识到synchronized不是解决多线程问题的万能钥匙。我之前维护过一个热点商品库存服务高峰期几十个线程挤在同一个监视器上抢一把锁接口吞吐量被锁竞争按在地上摩擦线程反复挂起、唤醒大量时间耗在系统调用和上下文切换上。后来把扣减逻辑改成基于CASCompare-And-Swap的原子类实现整个瓶颈才真正解开。这篇文章就围绕无锁编程的核心CAS、Java原子类的选型以及无数人面试翻车的ABA问题展开。适合刚入门并发编程的同学建立底层认知也适合准备Java面试或者在高并发系统里摸爬滚打的工程师来一次系统性复盘。1. CAS原理拆解无锁并发的起点1.1 先从锁竞争的下限说起我用一个最直观的对比来解释为什么锁有上限。synchronized在竞争激烈的时候没有抢到锁的线程会被操作系统挂起进入BLOCKED状态之后锁释放又要把它唤醒。这个挂起-唤醒的过程涉及用户态与内核态的切换一次切换的开销可能是微秒级别。当锁被几百个线程高频争抢时CPU时间不是在干活而是在陪操作系统做调度。无锁编程的思路完全不一样它不让线程去睡觉而是让线程在修改失败后立刻重试。这样做的前提是硬件必须提供一种“比较并交换”的指令能力也就是CAS。Java并发包里的原子类几乎全都建立在CAS之上所以标题里把CAS和原子类放在一起本质上讲的是同一套地基。这里还有个容易被忽略的点JVM内部的轻量级锁也早就用了CAS。HotSpot在锁竞争不激烈的时候允许线程先自旋抢锁自旋抢不到才升级为重量级锁。也就是说即使是你看得到、也用得着的synchronized底层也在偷偷借用无锁编程的思想。1.2 CAS底层到底干了什么CAS的语义非常简单只看三个动作读取内存位置V的当前值把这个值和调用者传入的期望值A做比较如果相等说明数据没有被别的线程改动过允许将V更新成新值B如果不相等说明被改过了本次操作失败关键点在于以上三步必须是原子完成的。如果“比较”和“更新”之间存在窗口另一个线程插进来改动CAS就失去了意义。这个原子性怎么保证答案是CPU指令。在Java层面最终是sun.misc.Unsafe提供的native方法public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);到了JVM和CPU这一层x86架构下对应的是cmpxchg指令。如果是多核处理器指令前面还会加上lock前缀用来锁住总线或者缓存行确保跨核心之间的原子性。这就是CAS在底层最真实的模样。可以类比去银行柜台办业务柜员问“你是张三吗”你递上身份证号柜员确认系统里登记的号和身份证号一致才给你办如果不一致就让你重新排队。CAS的“重试”就是重新排队的代价。1.3 CAS的三座大山CAS不是银弹它有三座大山循环时间开销大大量线程同时失败重试极端情况下CPU空转严重只能保证一个共享变量的原子性多变量复合状态的一致性CAS处理不了ABA问题数据变了之后再变回来CAS无法感知这三座大山里第二座在工作中最容易被忽略。很多人以为用了两个AtomicInteger就能保证两个字段之间的一致性其实不行。比如账户信息里有余额和状态两个字段你分别做了原子化但“扣余额改状态”两步之间必须保证同时成功或同时失败两个独立的原子类根本做不到。常规做法是把多个字段封装到一个POJO里然后用AtomicReference指向这个对象每次修改都构造一个新对象整体CAS替换。2. Java原子类全景不同场景该选谁2.1 基础原子类AtomicInteger的日常用法AtomicInteger是使用频率最高的基础原子类它的incrementAndGet()并不是像volatile int那样直接自增的底层等价于一个CAS循环public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v getIntVolatile(o, offset); } while (!weakCompareAndSetInt(o, offset, v, v delta)); return v; }先把当前值读出来然后CAS到“当前值1”如果失败说明有别的线程抢先改了重新读、重新计算一直循环到成功为止。日常开发里我没有身份验证、秒杀、统计类需求都会优先想到原子类。比如点赞数public class LikeService { private final AtomicInteger likeCount new AtomicInteger(); public int addLike() { return likeCount.incrementAndGet(); } public int getCount() { return likeCount.get(); } }这个写法在单机高并发场景下远胜于synchronized包裹的整形自增。当然如果流量已经大到需要Redis/ZooKeeper或者数据库乐观锁去协调多台机器那是另一个话题单机原子类解决不了分布式问题。2.2 引用原子类AtomicReference与自旋锁比基础原子类更底层、也更有发挥空间的是AtomicReference。它能让一个对象的引用被原子地更新。典型场景是实现一个自旋锁public class SpinLock { private final AtomicReferenceThread owner new AtomicReference(); public void lock() { Thread current Thread.currentThread(); while (!owner.compareAndSet(null, current)) { // 自旋等待临界区极短时才建议用 } } public void unlock() { Thread current Thread.currentThread(); owner.compareAndSet(current, null); } }这个锁的本质是哪个线程能通过CAS把owner从null改成自己谁就拿到锁。第一次见的同事容易问为什么不用volatile boolean因为CAS能保证“判断是否空锁和设置持有者”是一个原子动作还能防止别的线程误释放锁——只有持有锁的线程才能把锁放回去。要提醒的是自旋锁只适合临界区极短、没有IO、没有等待的场景。如果你的临界区里有数据库查询或者网络调用别用自旋锁CPU会被空转消耗干净。AtomicReference还有一个价值它可以配合不可变对象让“多字段复合状态”变得原子化。例如一个坐标对象Point(x, y)要同时更新x和y就构造新的Point并整替换AtomicReferencePoint pointRef new AtomicReference(new Point(0, 0)); int oldX pointRef.get().x; Point newPoint new Point(oldX 1, oldY 1); pointRef.compareAndSet(pointRef.get(), newPoint);2.3 高并发计数器LongAdder为什么更快如果任务只是“频繁自增、偶尔读取”比如网关的请求计数、QPS统计直接用AtomicLong在高竞争下会非常吃力因为所有线程都在同一个内存地址上打架。这时候LongAdder的价值就出来了。LongAdder的底层是Striped64它在内部维护了一个base字段和一个Cell[]数组。低并发时只更新base一旦检测到竞争激烈会把累加操作分散到不同的Cell上每个线程命中不同的Cell互相之间不冲突。求和时才把所有Cell的值累加起来。LongAdder counter new LongAdder(); // 自增 counter.increment(); // 取值 long total counter.sum();这个设计非常像“分段锁”的思路但它不是加锁而是分散并发点。要注意sum()遍历Cell求和时可能有线程正在更新Cell所以sum()得到的结果是弱一致性的不是精确快照更适合做统计而非强一致计数。面试里常问“LongAdder和AtomicLong怎么选”我习惯这样答数据结构一致性要求极高、更新频率没那么疯狂就选AtomicLong如果允许最终一致、追求高吞吐的统计场景选LongAdder。3. 无锁编程实操手写一个无锁栈3.1 Treiber Stack的设计思路理论讲再多不如自己动手写一个无锁数据结构。计算机科学家R. Kent Treiber在1986年提出了一个基于CAS的无锁栈实现——Treiber Stack。思路很直白栈顶用AtomicReference持有push时把新节点指向当前栈顶然后CAS替换栈顶为新节点pop时读取栈顶CAS替换为栈顶的下一个节点。如果CAS失败说明有其他线程抢先动了栈循环重试。这个栈的核心逻辑其实就两件事读快照、CAS推进。它不需要锁只依赖CPU的原子操作。3.2 朴素版本的实现与核心代码先写一个朴素版本不谈ABA问题只讲结构public class LockFreeStackT { private static class NodeT { final T value; NodeT next; Node(T value) { this.value value; } } private final AtomicReferenceNodeT head new AtomicReference(); public void push(T value) { NodeT newNode new Node(value); while (true) { NodeT top head.get(); newNode.next top; if (head.compareAndSet(top, newNode)) { return; } } } public T pop() { while (true) { NodeT top head.get(); if (top null) { return null; } NodeT newHead top.next; if (head.compareAndSet(top, newHead)) { return top.value; } } } }代码本身很短但有几个细节值得琢磨newNode.next在CAS成功之前被反复修改这是安全的因为每一次修改都基于最新的栈顶Node.next不需要声明为volatile因为节点只有到达栈顶后才会被别人读到而栈顶的发布行为由head.compareAndSet保证pop操作必须在CAS成功后才返回值不能只在读取后就返回否则可能把另一个线程已经pop掉的值当成自己的结果这个朴素版本跑多线程压测正常弹栈入栈没问题。但如果有人把节点放进对象池复用或者栈中节点被物理复用就会踩到ABA问题下一章专门讲。3.3 无锁队列的延伸思考写会无锁栈之后顺手理解无锁队列会容易很多。Michael-Scott队列是另一个经典的无锁队列它使用两个原子引用一个指向队列头、一个指向队列尾。入队时CAS把新的尾节点挂到当前尾节点后面出队时CAS把头节点移动到下一个节点。不过无锁队列的复杂度和坑明显比无锁栈高一个量级尤其是处理“哨兵节点”和“中间状态”时需要非常小心。我的建议是实战中没有必要非要自己造无锁队列JDK里的ConcurrentLinkedQueue已经是非常成熟的工业级实现读它的源码比重新造轮子更有价值。4. ABA问题破解版本号的正确姿势4.1 ABA问题到底是怎么发生的假设线程1从AtomicReference中读到值A在做下一步CAS之前被调度器挂起。线程2趁这个间隙把A改成B然后又改回A。线程1恢复后执行compareAndSet(A, ...)发现期望值和当前值一致CAS成功。从结果看值确实是A没有变过——但历史已经被改了两轮。如果业务逻辑关心的是“这个值是否有过变化”而不是“最终值等于什么”问题就来了。我举一个很通俗的例子椅子的归属。线程1看到椅子上的人是张三准备做记录“张三坐在这”。线程2让张三离开换李四坐了一会儿又让李四离开最后张三又坐回来。线程1醒来看到人还是张三直接写下“张三没离开过”。但事实上中间已经发生过一次甚至多次人员替换可能是安全隐患。代码层面可以这样演示AtomicReferenceString ref new AtomicReference(A); // 线程1 String current ref.get(); Thread.sleep(100); // 模拟挂起 boolean ok ref.compareAndSet(current, A2); // 线程2 ref.compareAndSet(A, B); ref.compareAndSet(B, A);线程1的CAS一定能成功但它完全没察觉到线程2的两次修改。在真实业务里无锁栈的节点复用、缓存热更新、账户余额补偿操作都可能触发类似问题。4.2 用AtomicStampedReference加版本号ABA的破解思路是不要只比较值还要比较版本号。AtomicStampedReference内部用一个不可变的Pair(reference, stamp)来保存引用和版本戳每次有意义的变化就“冲洗”一次版本戳。用它改造上面的程序AtomicStampedReferenceString ref new AtomicStampedReference(A, 0); int[] stampHolder new int[1]; String current ref.get(stampHolder); int currentStamp stampHolder[0]; Thread.sleep(100); // 模拟挂起 boolean ok ref.compareAndSet(current, A2, currentStamp, currentStamp 1); System.out.println(CAS是否成功: ok);此时线程2做了两次修改每次版本号都加1线程1持有的currentStamp0已经过期CAS会失败从而识别出数据中间被改动过。这个方案在无锁栈上同样有效。把栈顶从AtomicReference换成AtomicStampedReference每次push和pop都让版本号自增public class SafeStackT { private static class NodeT { final T value; final NodeT next; Node(T value, NodeT next) { this.value value; this.next next; } } private final AtomicStampedReferenceNodeT head new AtomicStampedReference(null, 0); public void push(T value) { while (true) { int[] stamp new int[1]; NodeT top head.get(stamp); NodeT newNode new Node(value, top); if (head.compareAndSet(top, newNode, stamp[0], stamp[0] 1)) { return; } } } public T pop() { while (true) { int[] stamp new int[1]; NodeT top head.get(stamp); if (top null) { return null; } if (head.compareAndSet(top, top.next, stamp[0], stamp[0] 1)) { return top.value; } } } }有人会问版本号自增这么多次会不会溢出理论上会但实际情况中2^31次更新需要非常漫长的运行时间而且溢出后重新从负数开始java的循环技术也把int情况变成循环使用现实运维通常不会跑那么久才重启所以不用过度担心。4.3 AtomicMarkableReference的取舍很多时候我们不需要那么详细的版本号只需要知道“这个对象有没有被打过标”或者“这个节点有没有被删过”。这时候AtomicMarkableReference更合适它只以boolean作为标记表示“当前引用是否处于已标记状态”。一个典型场景是并发删除标记链表中的某个节点被逻辑删除后打上标记。其他线程看到标记就不会再对这个节点做操作。它不关心标记被切换了多少次只关心当前状态是哪一种。所以选型很简单关心“被改了多少次”或者“版本变迁历史”用AtomicStampedReference只关心“有没有发生过某类状态变化”用AtomicMarkableReference从内存占用看Markable也更省空间因为它只需要一个布尔位。5. 实际项目中踩过的坑与性能实测5.1 自旋导致CPU飙高的处理线上真正踩过最稳的坑是某次把库存扣减从synchronized换成AtomicInteger之后压测时发现CPU利用率异常飙升。追问原因热点库存一旦竞争空前激烈大量线程的CAS连续失败它们不会立刻退出循环而是忙等这比阻塞更消耗CPU。解决办法主要有三个引入退避策略CAS失败后调用Thread.onSpinWait()这是Java 9开始提供的基础指令能提示CPU当前处于自旋热循环中降低指令流水线的消耗失败次数的退避上限之后改用Thread.yield()让出CPU时间片给其他线程喘息机会如果竞争还是压不住说明CAS已经不适合这个场景直接用LongAdder或者干脆用锁更合理我自己常写的一个带退避的通用模板int spins 0; while (true) { if (casSuccess) { return true; } if (spins MAX_SPIN) { Thread.yield(); spins 0; } }5.2 只保证单变量一致性导致的误用另一个很容易踩的坑是我上面提过的“多个独立原子类的事务性问题”。有一个项目用户兑换积分时要同时更新积分账户和优惠券状态开发同事用两个AtomicInteger维护。结果极端情况下积分扣减成功但优惠券状态失败因为两个CAS之间没有整体性。这个场景的正确解法是要么用AtomicReferenceAccountState把一个不可变对象原子替换掉要么回到锁的怀抱用一段同步代码块保证多变量更新不可分割。CAS确实很快但要清楚它的边界。单变量的原子性它负责复合操作的一致性它不管。5.3 性能实测与选型建议我在8核的Linux机器上做过一个简单压测8条线程对同一个计数器各自增1000万次。结果很典型无锁变量用AtomicLong耗时明显偏高因为8条线程全部挤在同一个内存地址上做CAS用LongAdder耗时快了很多因为竞争被分散到多个Cell里低并发1-2线程下反而AtomicLong更快因为LongAdder维护Cell数组本身有开销所以不要迷信某一个类选型要看并发量和一致性要求。场景推荐方案理由计数、并发量低、要求强一致AtomicLong / AtomicInteger简单可靠低竞争下开销最小高频统计、允许弱一致LongAdder / LongAccumulator分散竞争点吞吐量明显更高引用对象CAS、自旋锁AtomicReference可原子更新对象引用需要感知修改历史/次数AtomicStampedReference版本戳可破解ABA问题只需判断是否改动过AtomicMarkableReference布尔标记足够更加轻量从工程复杂度看优先使用JDK已经封装好的原子类比如ConcurrentHashMap、ConcurrentLinkedQueue、LongAdder它们比绝大多数手写无锁结构要成熟得多。无锁编程最大的价值体现在底层组件和中间件里业务开发真正需要亲自造的不多但把原理吃透能让你写出更稳、更能解释问题的代码。我个人在实际操作中的体会是CAS和原子类不是万能灵药但理解它们会从根本上改变你对并发的思考模式。建议每个做Java并发开发的同事都自己动手写一遍Treiber Stack跑一次多线程压测亲眼看看ABA问题在你面前发生是什么感觉。这个坑踩过一次比看十篇原理分析都管用。

相关新闻

ERNIE情感分析实战:从句子级到属性级微调全流程

ERNIE情感分析实战:从句子级到属性级微调全流程

简介:基于Python与预训练大模型ERNIE的情感分析项目源码及配套数据集,完整覆盖句子级与属性级两类情感分析任务,面向自然语言处理学习者、算法工程师以及有实际情感分析需求的开发者。项目从数据预处理、加载ERNIE模型、特征提取,…

2026/10/7 13:27:25 阅读更多 →
知识库向量数据库怎么选?我折腾了一圈,答案就三个字

知识库向量数据库怎么选?我折腾了一圈,答案就三个字

事情是这样的。上一篇文章聊完三层检索策略之后,我盯着终端里的 docker ps 出神——里面躺着一个 Qdrant 容器,跑了快一个月,CPU 占用稳如老狗,但我一次查询都没往它那边发。因为我发现我的 Chroma 跑得好好的。240 篇文章&#x…

2026/10/7 13:27:25 阅读更多 →
网络搭建及应用赛项备赛指南:视频讲解与答案的实战用法

网络搭建及应用赛项备赛指南:视频讲解与答案的实战用法

简介:面向全国职业院校技能大赛《网络搭建及应用》赛项的备赛者与指导教师,这份资料完整整理了2021年赛题的视频讲解、操作录屏与参考答案。压缩包共60个文件,以38个txt配置记录、11个docx报告与分解笔记、8个Python脚本、2个PDF说明及1个mp4…

2026/10/7 13:27:25 阅读更多 →

最新新闻

Agent-Reach:面向生产环境的LLM API治理与智能路由网关

Agent-Reach:面向生产环境的LLM API治理与智能路由网关

1. 项目概述:Agent-Reach 是什么,它解决的不是“能不能用”,而是“怎么用得稳、用得准、用得省”Agent-Reach 这个名字乍看像某个开源模型或框架,但结合它在 CLI、API、YouTube、Reddit 等关键词中的高频共现,再叠加近…

2026/10/7 14:14:10 阅读更多 →
OpenCode 高阶玩法:CLI 自动化、CI/CD 集成与远程协作

OpenCode 高阶玩法:CLI 自动化、CI/CD 集成与远程协作

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

2026/10/7 14:14:10 阅读更多 →
Codex 驱动 R 语言数据实战指南:把 auth.json 改到 TaoToken

Codex 驱动 R 语言数据实战指南:把 auth.json 改到 TaoToken

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

2026/10/7 14:14:10 阅读更多 →
MySql数据库与python交互查询与封装(十二):用TaoToken统一Key打通查询链路

MySql数据库与python交互查询与封装(十二):用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/7 14:14:10 阅读更多 →
Cursor 在 Android 开发中的实战:把 Base URL 改到 TaoToken 的配置与验证

Cursor 在 Android 开发中的实战:把 Base URL 改到 TaoToken 的配置与验证

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

2026/10/7 14:14:10 阅读更多 →
90DaysOfDevOps 实践笔记:使用 Docker Compose 本地部署 ELK Stack 并完成日志可视化

90DaysOfDevOps 实践笔记:使用 Docker Compose 本地部署 ELK Stack 并完成日志可视化

文档/教程 【免费下载链接】90DaysOfDevOps This repository started out as a learning in public project for myself and has now become a structured learning map for many in the community. We have 3 years under our belt covering all things DevOps, including Pri…

2026/10/7 14:13:09 阅读更多 →

日新闻

ROS2机械臂仿真与运动控制:从URDF建模到Gazebo实战全解析

ROS2机械臂仿真与运动控制:从URDF建模到Gazebo实战全解析

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

2026/10/7 1:01:58 阅读更多 →
用浏览器直接改ESP32的WiFi密码:NVS键值配置工具设计与实现

用浏览器直接改ESP32的WiFi密码:NVS键值配置工具设计与实现

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

2026/10/7 1:02:00 阅读更多 →
芯片封装缺陷检测:扫描声学显微镜(SAT)原理与实操指南

芯片封装缺陷检测:扫描声学显微镜(SAT)原理与实操指南

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

2026/10/7 1:02:00 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

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

2026/10/6 7:15:40 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

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

2026/10/6 5:29:09 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

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

2026/10/7 9:29:10 阅读更多 →

月新闻

我发现了一个新思路:用 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/6 8:21:32 阅读更多 →
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/7 11:43:46 阅读更多 →
黑夜航拍船只数据集训练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/7 13:34:55 阅读更多 →