cas是实现无锁数据结构的核心机制,通过原子性“比较并交换”避免互斥锁,减少高并发下的阻塞与上下文切换;其关键在于构建可重试、无死锁的逻辑,如基于哨兵节点的单向链表无锁队列,依赖head/tail原子指针与循环cas完成入队出队。

CAS 是实现无锁数据结构的核心机制,它通过原子性地“比较并交换”内存值,避免使用互斥锁,从而在高并发下减少线程阻塞和上下文切换开销。关键不在于 CAS 本身多复杂,而在于如何用它构建安全、可重试、无竞争死锁的逻辑。
用 CAS 构建无锁队列的基本思路
无锁队列通常基于单向链表(带哨兵头结点),靠两个原子指针控制:head(出队端)和 tail(入队端)。所有修改都通过 CAS 循环重试完成:
- 入队(Enqueue):先用 CAS 将新节点拼到 tail->next;若失败(tail 已被其他线程更新),就先推进 tail 指针再重试;成功后,再用 CAS 更新 tail 指向新节点
- 出队(Dequeue):读取 head->next 节点;若非空,用 CAS 尝试将 head 指向该节点的 next;失败则重试;成功后返回原 head->next 的 item
- 哨兵节点很重要:它让 head 和 tail 初始指向同一节点,消除了空队列的特殊判断,统一了操作路径
无锁链表的关键设计细节
单向无锁链表比双向更常用,因为指针更新更少、ABA 问题更容易缓解:
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
-
节点结构需支持原子更新 next 指针:C++ 用
std::atomic<node></node>,Java 用AtomicReference,Go 用atomic.CompareAndSwapPointer - 插入/删除必须是“查找 + CAS”两步:先遍历定位前驱节点,再对前驱的 next 字段做 CAS;若中间被其他线程修改,就从头开始查找
- 删除节点时不能直接释放内存:要等所有可能正在访问它的线程退出(需配合 RCU 或 Hazard Pointer 等内存回收机制),否则出现悬垂指针
绕不开的几个现实问题及应对方式
CAS 不是银弹,实际落地必须处理这些典型挑战:
-
ABA 问题:某节点地址被 A→B→A 复用,CAS 误判为未变。解决方案包括:给指针附加版本号(如
AtomicStampedReference)、用 128 位 CAS(如 C++20std::atomic<pair int>></pair>) - 忙等待开销:CAS 失败后自旋重试,在高竞争下浪费 CPU。可加入短暂 pause/yield,或退化为轻量锁作为 fallback
- 复合操作无法原子化:比如“先查再删”不是原子的。只能靠循环重试 + 状态标记(如节点加 deleted 标志)来逼近线性一致性
语言层面的典型支持方式
不同语言提供不同粒度的 CAS 原语,选型直接影响实现难度:
-
C++:依赖
std::atomic<t>::compare_exchange_weak/strong</t>,支持任意类型(需 trivially copyable),配合memory_order控制内存序 -
Java:
Unsafe.compareAndSwapObject或更高层的AtomicReferenceFieldUpdater,JDK 自带的ConcurrentLinkedQueue就是经典范例 -
Go:
sync/atomic.CompareAndSwapPointer或atomic.Int64等封装类型,但不支持泛型指针 CAS,常配合unsafe.Pointer使用










