核心手段,使用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):
- 读取
head = Node1(旧值记为:old_head = Node1) - 读取
old_head.next = Node2(新队头应该是Node2) - T1暂时被操作系统挂起(还没执行CAS)
此时T1的预期:把head从Node1改成Node2。
当前队列:
head → Node1 → Node2 → null
tail → Node2
步骤2:线程T2、T3疯狂操作,把A→B→A
T2、T3正常执行,连续做2次出队 + 1次入队:
-
T2出队 :CAS成功把
head从Node1→Node2
队列:head→Node2→null,tail→Node2 -
T3出队 :CAS成功把
head从Node2→null
队列:空队列 (head=null, tail=null) -
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把head从Node3强行改成了Node2,最终队列变成:
head → Node2(已被出队的废弃节点) → null
tail → Node3
问题:
- 数据丢失 :新入队的
Node3被孤立,永远访问不到 - 野指针 :
head指向已经出队、甚至被回收的Node2 - 死循环/崩溃:后续出队会访问无效内存
核心原因总结
CAS只比较指针的内存地址/值,不关心:
- 这个值中间有没有被修改过
- 对应的节点是否已经被出队、销毁
- 队列的结构是否发生了本质变化
只要最后值变回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
总结
- ABA问题本质:CAS只认值,不认过程,值改回原值就会误判
- 队列场景危害:导致指针指向废弃节点、数据丢失、程序崩溃
- 根治方案 :版本号/邮戳,让每次修改都有唯一标识
- 核心逻辑:不仅比较值,还要比较"修改次数",确保中间没被篡改并 delete,T1 后续访问会触发野指针)。
解决方案与 ABA 问题重叠(风险指针、时代回收),核心思想是延迟回收,确保节点不再被任何线程引用后再释放。
具体示例: