无锁队列细节

核心手段,使用CAS锁,compare and swap,判断head、tail指针是否和修改前相同,若相同则修改,否则则重试

CAS动作是一个原子动作,比较和修改是一起原子执行的,中间不可能插入其他线程

多生产者单消费者

多个生产者线程判断是否当前tail指针被修改了,如果没有被修改那么直接设置为新的node,并原子获取tail之前的指针pre

CAS原子动作结束后,再修改pre->next = cur

这种执行可能有一个窗口,导致链表发生断裂。

如果不想链表发生断裂,可以先CAS修改tail->next为当前node(比较是否为null,不为空就失败),成功之后再CAS修改tail为当前node(比较是否为查询到的tail)

为什么多消费多生产者模式比较复杂

无锁队列 多生产者的问题

无锁队列的设计核心是用 acquire/release 内存序替代 seq_cst,在保证正确性的前提下优化性能。

二、无锁队列的核心设计难点

2.1 ABA 问题(最致命)

问题描述

多线程并发修改时,原子变量的值从 A 被改为 B,再改回 A,导致依赖该值的线程误以为"值未变化",从而执行错误操作。

示例(队列头节点删除):

线程 T1 读取队列头 head = A,准备通过 CAS 删除 A(将 head 改为 A->next);

线程 T2 插入节点 B(head = B,B->next = A),随后删除 B(head = A);线程 T1 恢复执行,CAS 检查 head == A(条件成立),将 head 改为 A->next,但此时 A 可能已被 T2 回收(悬空指针),或 A->next 已失效,导致崩溃。(看文末的例子更好懂)

解决方案

标记指针(Tagged Pointer):给指针附加一个版本号(如 std::atomic<std::pair<Node*, uint64_t>>),每次修改指针时版本号递增。CAS 时同时检查指针和版本号,即使指针相同,版本号不同也会拒绝更新。

风险指针(Hazard Pointers):线程访问节点前,将节点指针存入"风险指针",确保节点被访问期间不会被回收。

基于时代的回收(Epoch-Based Reclamation):按"时代"划分线程操作,仅回收所有线程都已退出该时代的节点。

2.2 内存回收问题

无锁队列的节点是动态分配的,但不能直接 delete------可能有其他线程仍在访问该节点(如 T1 读取节点后,T2 删除节点

无锁队列CAS的ABA问题:通俗例子+队列场景详解

先一句话说清:ABA问题就是CAS操作只看"值是否相等",不看"值的变化过程" ------ 线程1看到值是A,中间被其他线程改成B又改回A,线程1以为没变化,直接CAS成功,但队列的结构/数据已经被篡改了,会导致队列错乱、数据丢失、死循环。

下面用最贴合你说的场景(入队队尾、出队队头),一步步模拟无锁单向链表队列的ABA问题。


前置知识:无锁队列的核心结构

我们用单向链表实现无锁队列,核心两个指针:

  • head:队头指针(出队用)
  • tail:队尾指针(入队用)
  • CAS操作:compare_and_swap(指针, 旧值, 新值) → 只有指针当前值=旧值时,才更新为新值

初始队列状态:

复制代码
head → Node1(数据:A) → Node2(数据:B) → null
tail → Node2

(队头:Node1,队尾:Node2)


完整ABA问题场景(出队操作触发)

步骤1:线程T1准备出队,读取状态

T1要执行出队操作(取出队头Node1):

  1. 读取 head = Node1(旧值记为:old_head = Node1
  2. 读取 old_head.next = Node2(新队头应该是Node2)
  3. T1暂时被操作系统挂起(还没执行CAS)

此时T1的预期:把headNode1改成Node2

当前队列:

复制代码
head → Node1 → Node2 → null
tail → Node2

步骤2:线程T2、T3疯狂操作,把A→B→A

T2、T3正常执行,连续做2次出队 + 1次入队

  1. T2出队 :CAS成功把headNode1Node2
    队列:head→Node2→nulltail→Node2

  2. T3出队 :CAS成功把headNode2null
    队列:空队列head=null, tail=null

  3. T3入队新节点 :新建节点Node3(数据:A),放入队尾
    最终队列:

    复制代码
    head → Node3(数据:A) → null
    tail → Node3

✅ 关键:新的head又变成了A(Node3) ,和T1最开始看到的old_head=Node1(A)值完全一样


步骤3:线程T1恢复执行,CAS错误成功

T1被唤醒,执行CAS:

java 复制代码
// T1的代码
compare_and_swap(head, old_head(=Node1), Node2)

因为当前head=Node3(A),和old_head=Node1(A)值相等 ,CAS无脑成功


灾难后果:队列彻底损坏

T1把headNode3强行改成了Node2,最终队列变成:

复制代码
head → Node2(已被出队的废弃节点) → null
tail → Node3

问题:

  1. 数据丢失 :新入队的Node3被孤立,永远访问不到
  2. 野指针head指向已经出队、甚至被回收的Node2
  3. 死循环/崩溃:后续出队会访问无效内存

核心原因总结

CAS只比较指针的内存地址/值,不关心:

  1. 这个值中间有没有被修改过
  2. 对应的节点是否已经被出队、销毁
  3. 队列的结构是否发生了本质变化

只要最后值变回A,CAS就认为"没问题",这就是ABA问题。


解决方案(无锁队列标准解法)

1. 版本号机制(最常用)

head/tail指针绑定一个递增的版本号 ,结构变成:
(指针值, 版本号) → 原子操作同时CAS两个值

例子:

  • T1读取:(Node1, 版本1)
  • T2/T3修改后:(Node3, 版本4)
  • T1 CAS:版本1≠4,直接失败,重新尝试

✅ 哪怕指针值都是A,版本号一定不同,彻底杜绝ABA。

2. Java中的实现

  • JDK AtomicStampedReference:带**邮戳(版本号)**的原子引用
  • JDK ConcurrentLinkedQueue:无锁队列,通过节点状态标记+next指针规避ABA

总结

  1. ABA问题本质:CAS只认值,不认过程,值改回原值就会误判
  2. 队列场景危害:导致指针指向废弃节点、数据丢失、程序崩溃
  3. 根治方案版本号/邮戳,让每次修改都有唯一标识
  4. 核心逻辑:不仅比较值,还要比较"修改次数",确保中间没被篡改并 delete,T1 后续访问会触发野指针)。

解决方案与 ABA 问题重叠(风险指针、时代回收),核心思想是延迟回收,确保节点不再被任何线程引用后再释放。

具体示例:

相关推荐
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之字符串 --【字符串基础】:输出亲朋字符串
c++·字符串·csp·高频考点·信奥赛·专项训练·输出亲朋字符串
WBluuue2 小时前
数据结构与算法:莫队(一):普通莫队与带修莫队
c++·算法
KuaCpp3 小时前
C++面向对象(速过复习版)
开发语言·c++
智者知已应修善业6 小时前
【51单片机不用数组动态数码管显示字符和LED流水灯】2023-10-3
c++·经验分享·笔记·算法·51单片机
AI进化营-智能译站6 小时前
ROS2 C++开发系列16-智能指针管理传感器句柄|告别ROS2节点内存泄漏与野指针
java·c++·算法·ai
报错小能手6 小时前
好好讲讲移动构造 移动赋值
c++
syker7 小时前
AIFerric深度学习框架:自研全栈AI基础设施的技术全景
开发语言·c++
xvhao20137 小时前
单源、多源最短路
数据结构·c++·算法·深度优先·动态规划·图论·图搜索算法
笑鸿的学习笔记9 小时前
qt-C++语法笔记之Qt Graphics View 框架中的类型辨析完全指南
c++·笔记·qt