细粒度锁必须锁定相邻两节点,因插入删除需修改前驱的next指针,仅锁单节点会导致链表断裂;std::atomic不能替代互斥锁,因其无法原子执行读-改-写序列且shared_ptr引用计数非真正无锁。

细粒度锁为什么不能只锁节点,而要锁相邻两节点
单向链表的插入、删除操作天然需要修改前驱节点的 next 指针,比如在节点 A 后插入 B,本质是把 A->next 从指向 C 改为指向 B。如果只锁目标节点 B 或 C,A 的 next 字段仍可被其他线程并发修改,导致链表断裂或重复释放。
正确做法是:对任意位置的操作(查、插、删),都需锁定「待操作节点的前驱」和「该节点本身」——即两个连续节点。这是细粒度锁能保证线性一致性的最小锁粒度。
- 查找时:锁 head → 锁 next → …… 直到命中或到尾,途中释放已遍历过的前驱锁(但保留当前节点锁用于后续修改)
- 插入时:定位到前驱 P 和目标位置(P->next),锁 P 和 P->next,再修改 P->next
- 删除时:同样锁 P 和 C(C = P->next),确认 C 未被其他线程标记为待删后,再执行 P->next = C->next
std::atomic<:shared_ptr>></:shared_ptr> 能否替代细粒度互斥锁
不能直接替代,但可辅助实现无锁(lock-free)版本;用在有锁实现里反而容易出错。
std::atomic<:shared_ptr>></:shared_ptr> 只能原子地读写单个指针字段(如 next),无法原子地完成「读 A->next,再写 A->next」这样的读-改-写序列。即使 next 是原子的,多个线程仍可能在读取同一 next 后各自构造新节点并写回,造成 A->next 被覆盖两次,丢失一次插入。
立即学习“C++免费学习笔记(深入)”;
更关键的是:shared_ptr 的引用计数递增/递减本身不是无锁的,高竞争下会退化为内部锁,失去性能优势;且它不解决内存重排序问题(比如新节点字段未完全初始化就被 next 指向)。
- 若真要用原子指针,必须配合
std::atomic_thread_fence+ Hazard Pointer / RCU 等内存安全机制 - 普通业务场景中,
std::mutex配合双节点锁更易验证、调试成本低、实际吞吐也不差 - 别把
atomic<shared_ptr></shared_ptr>当成“无锁银弹”——它连最基本的fetch_add都不支持
删除操作中 ABA 问题的真实影响与规避方式
在细粒度锁链表中,ABA 本身不构成问题——因为锁已保证操作期间节点不会被释放或复用。但一旦引入原子指针(例如用 compare_exchange_weak 实现无锁删除),ABA 就立刻致命:线程 T1 读到 A->next == B,T2 删除 B 并释放其内存,T3 分配新节点恰好复用同一地址 B',T1 再次 compare_exchange 会成功,却把 A->next 指向一个语义上完全无关的新对象。
标准解法是「版本号+指针」组合(如 std::atomic<uintptr_t></uintptr_t> 存储高位版本、低位指针),但要注意:
- 版本号必须随每次写入递增,不能仅靠节点生命周期管理
- x86_64 上
cmpxchg16b指令非全平台支持,需运行时检测或降级为带锁路径 - 即便用了版本号,仍需确保节点内存不被提前回收——得搭配 Hazard Pointer 或 epoch-based reclamation
实际编码时最容易漏掉的三个内存安全点
很多开源实现在线程安全上看似完整,却在以下三处栽跟头:
- 节点构造后、插入前,未用
std::atomic_thread_fence(std::memory_order_release)刷出所有字段写入——导致其他线程看到next已更新,但节点内数据仍是未初始化垃圾值 - 删除节点后直接
delete node,但此时可能仍有其他线程正持有该节点的shared_ptr或 raw pointer 并准备访问其字段——必须等所有潜在访问者明确退出临界区(用锁或 hazard pointer 等待) - 遍历链表时用
while (p != nullptr) { ... p = p->next; },但p->next是std::atomic<node></node>类型,必须显式调用.load(),否则触发隐式转换失败或未定义行为
这些点不报编译错误,但会在高并发压力下以极低概率崩溃或静默损坏数据,复现困难。建议所有指针字段统一用 std::atomic<node></node> 声明,并强制走 .load()/.store()。


















