不能直接用std::atomic指针CAS完事,因单向链表插入/删除需原子更新前驱next和当前节点next,裸CAS无法防止ABA问题与中间态撕裂;必须配合compare_exchange_weak循环重试、所有next声明为atomic、并引入hazard pointer等内存回收机制。

为什么不能直接用 std::atomic 对指针做 CAS 就完事?
因为单向链表的插入/删除必须保证两个动作原子:一是更新当前节点的 next 指针,二是让前驱节点的 next 指向它(或跳过它)。裸用 std::atomic<node>&</node> 只能保护单个指针读写,无法防止 ABA 问题和中间态撕裂。比如删除节点时,若另一个线程刚把该节点重新挂回去(内存复用),CAS 会误判成功。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 必须用
std::atomic<t>::compare_exchange_weak</t>配合循环重试,不能只调一次 - 所有涉及修改的指针字段(如
next)必须声明为std::atomic<node></node>,而非Node*加外部锁 - 节点构造后不能被外部直接 delete——需配合内存回收机制(如 epoch-based reclamation 或 hazard pointer),否则可能访问已释放内存
如何安全实现无锁 push_front(头插)?
头插是无锁链表最简单且最稳定的操作,只需一步 CAS:把新节点的 next 设为当前头,再用 CAS 把 head 原子更新为新节点。但要注意初始化和空链表边界。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- head 初始化为
nullptr,但注意std::atomic<node></node>默认构造即为nullptr - 插入循环中,先读取当前
head.load(),再设置new_node->next.store(old_head),最后用head.compare_exchange_weak(old_head, new_node) - 如果
compare_exchange_weak失败,说明 head 已被其他线程改写,直接重试即可——无需锁、无需等待
示例关键片段:
struct Node {
int value;
std::atomic<Node*> next{nullptr};
};
<p>std::atomic<Node*> head{nullptr};</p><div class="aritcle_card flexRow">
<div class="artcardd flexRow">
<a class="aritcle_card_img" href="/xiazai/gongju/2823" title="C++14"><img
src="https://img.php.cn/upload/manual/001/431/639/6ac8b33c327c4749.png" alt="C++14" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/xiazai/gongju/2823" title="C++14">C++14</a>
<p>C++14 对 C++11 的修正与增强版本,适合旧系统维护和较老工具链兼容。</p>
</div>
<a href="/xiazai/gongju/2823" title="C++14" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div><p>void push_front(int v) {
Node<em> n = new Node{v, nullptr};
Node</em> expected;
do {
expected = head.load();
n->next.store(expected);
} while (!head.compare_exchange_weak(expected, n));
}pop_front 为什么比 push_front 危险得多?
头删需要读 head、读 head->next、再 CAS head 到 head->next——这三步之间存在竞态窗口。典型错误是:线程 A 读到 head=X,线程 B 删除 X 并释放内存,线程 A 接着解引用 X->next,触发 UAF(Use-After-Free)。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 仅靠
std::atomic无法解决内存生命周期问题,必须引入回收机制 - 最轻量方案是使用
hazard pointer:每个线程在读 head 前,把自己的 hazard pointer 指向 head;删除前检查所有线程的 hazard pointer 是否还指向待删节点 - 若不实现回收机制,至少要把
pop_front改成「返回值 + 延迟 delete」,由上层统一管理内存(例如放进对象池),否则必崩
哪些场景下其实不该硬上无锁链表?
无锁结构不是性能银弹。当链表长度短(std::mutex 的开销远低于无锁逻辑的分支预测失败+缓存行颠簸+重试开销。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 先用带锁版本跑基准测试(如
std::list+ mutex),确认锁真成了瓶颈,再考虑无锁 - 避免在容器内部做复杂逻辑(如遍历查找、按值删除)——这些天然难无锁化,强行做只会放大 ABA 和内存安全风险
- 生产环境若需无锁链表,优先用成熟库(如
libcds的cds::intrusive::MichaelList),而不是手写
真正容易被忽略的,是内存回收那层薄薄的纸——它不报错,但一碰就段错误。

















