一、atomic概念
原子操作是指不可中断的操作,即一个操作的执行过程中,不会被其他线程打断,要么全部执行完毕,要么根本不执行。
举个简单例子:多线程环境下,对一个变量i执行i++操作,看似是一个单一操作,实则包含三个步骤:读取i的值、将i的值加1、将结果写回i。如果没有原子性保证,多个线程同时执行时,会出现线程安全问题(比如两个线程同时读取i=1,都加1后写回,最终i=2,而非预期的3)。
atomic的核心作用
-
解决多线程环境下的数据竞争问题,避免线程安全隐患;
-
替代 synchronized 锁,实现更高效的并发控制(atomic基于CAS机制,无锁操作,性能优于锁);
-
保证操作的原子性、可见性和有序性(部分场景下)。
二、atomic实现无锁队列
完整代码如下:
cpp
#include<iostream>
#include<atomic>
using namespace std;
// 队列节点结构
template<typename T>
struct Node {
T data; // 节点存储的数据
Node* next; // 指向后一个节点的指针
// 节点构造函数,初始化数据和next指针
Node(T val) : data(val), next(nullptr) {}
};
// 无锁队列类(模板实现,支持任意数据类型)
template<typename T>
class LockFreeQueue {
private:
// 原子指针:head(队列头)、tail(队列尾)
atomic<Node<T>*> head;
atomic<Node<T>*> tail;
public:
// 构造函数:初始化队列(带头节点,避免空队列判断复杂)
LockFreeQueue() {
// 创建哑节点(dummy node),作为队列的哨兵节点
Node<T>* dummy = new Node<T>(T{});
// 原子存储:将head和tail都指向哑节点
head.store(dummy);
tail.store(dummy);
}
// 入队操作:将数据val加入队列尾部
void enqueue(T val) {
// 1. 创建新节点,存储待入队数据
Node<T>* newNode = new Node<T>(val);
// 2. 原子交换:将tail指向新节点,并返回原来的tail(oldTail)
// memory_order_acq_rel:保证原子操作的内存可见性和有序性
Node<T>* oldTail = tail.exchange(newNode, memory_order_acq_rel);
// 3. 将原来的尾节点的next指向新节点,完成链表链接
oldTail->next = newNode;
}
// 出队操作:将队列头部数据取出,存入val,返回是否出队成功
bool dequeue(T& val) {
// 1. 原子加载head指针(获取当前队列头)
Node<T>* oldHead = head.load(memory_order_acquire);
// 2. 获取head的下一个节点(真正存储数据的节点,哑节点不存数据)
Node<T>* next_node = oldHead->next;
// 3. 判断队列是否为空(next_node为nullptr,说明只有哑节点)
if (next_node == nullptr) {
return false; // 队列为空,出队失败
}
// 4. 取出next_node的数据(真正要出队的数据)
val = next_node->data;
// 5. 原子存储:将head指向next_node,完成头节点更新
head.store(next_node, memory_order_release);
// 6. 释放原来的头节点(哑节点),避免内存泄漏
delete oldHead;
return true; // 出队成功
}
// 析构函数:释放队列所有节点,避免内存泄漏
~LockFreeQueue() {
Node<T>* cur = head.load();
while (cur != nullptr) {
Node<T>* tmp = cur;
cur = cur->next;
delete tmp;
}
}
};
// 测试代码
int main()
{
// 实例化一个存储int类型的无锁队列
LockFreeQueue<int> q;
// 入队两个元素
q.enqueue(10);
q.enqueue(20);
int val;
// 出队并打印结果
if (q.dequeue(val))
{
cout << "Dequeued: " << val << endl;
}
if (q.dequeue(val))
{
cout << "Dequeued: " << val << endl;
}
return 0;
}
运行输出:
bash
Dequeued: 10 Dequeued: 20
代码拆解:
1 节点结构(Node)
队列采用单向链表实现,每个Node节点包含两个核心成员:
-
data:存储具体的数据,支持任意类型(通过模板T实现); -
next:指向后一个节点的指针,用于维护链表的连续性。

构造函数Node(T val)用于初始化节点,将数据val存入data,并将next指针置为nullptr(初始状态下无后续节点)。
2 无锁队列类(LockFreeQueue)
该类是核心,通过两个原子指针(atomic<Node<T>*>)实现无锁的入队和出队操作,避免了传统锁机制的开销。

2.1 私有成员:原子指针head和tail
核心设计:用atomic包装Node指针,保证对指针的读写操作是原子性的------即多个线程同时读写head/tail时,不会出现"读取一半被打断""写入冲突"等问题。
为什么用原子指针:普通指针的读写的非原子的,比如线程A读取head指针,线程B同时修改head指针,可能导致线程A读取到一个"无效指针"(只读取了指针的一半字节),引发程序崩溃。而atomic<Node*>会通过CPU原子指令,保证指针的读写操作完整、不可中断。
2.2 构造函数:初始化哑节点
队列初始化时,创建一个哑节点(dummy node),并将head和tail都指向这个哑节点。
设计哑节点的核心目的是避免空队列的复杂判断------如果没有哑节点,队列空时head和tail都为nullptr,入队、出队时需要额外判断nullptr,容易引发并发问题;有了哑节点,队列始终至少有一个节点(哑节点),head和tail永远不会为nullptr,简化了逻辑。
2.3 入队操作(enqueue)
入队操作是无锁队列的核心,全程无锁,通过原子操作保证线程安全,步骤拆解:
-
创建新节点:
Node<T>* newNode = new Node<T>(val);,将待入队的数据val存入新节点; -
原子交换tail指针:
tail.exchange(newNode, memory_order_acq_rel);-
exchange是atomic的核心成员函数:将tail的值(当前尾节点指针)替换为newNode,并返回替换前的tail值(oldTail); -
memory_order_acq_rel:内存序,保证该原子操作的"获取-释放"语义------确保所有线程能看到该操作之前的内存写入,同时该操作的写入能被后续线程看到,避免指令重排导致的并发问题。
-
-
链接新节点:
oldTail->next = newNode;,将原来的尾节点(oldTail)的next指针指向新节点,完成链表的链接,此时新节点成为新的尾节点。

为什么入队不需要锁?
因为tail.exchange是原子操作,多个线程同时入队时,只有一个线程能成功将tail替换为自己的新节点,其他线程会拿到更新后的tail(前一个线程的新节点),并将自己的新节点链接到该tail之后,不会出现节点链接混乱。
2.4 出队操作(dequeue)
出队操作同样基于原子操作,步骤拆解:
-
原子加载head指针:
Node<T>* oldHead = head.load(memory_order_acquire);,获取当前队列头(哑节点); -
获取有效数据节点:
Node<T>* next_node = oldHead->next;,next_node才是真正存储数据的节点(哑节点的next指向第一个有效节点); -
判断队列是否为空:如果next_node为nullptr,说明队列中只有哑节点,无有效数据,返回false(出队失败);
-
取出数据:
val = next_node->data;,将有效节点的数据存入val(通过引用传出); -
原子更新head指针:
head.store(next_node, memory_order_release);,将head指向next_node(新的哑节点); -
释放旧哑节点:
delete oldHead;,避免内存泄漏,返回true(出队成功)。

注意:memory_order_acquire(加载时)和memory_order_release(存储时)的配合,保证了出队操作的内存可见性------线程A出队后,线程B能立即看到head的更新,不会读取到旧的head指针。
3 测试代码(main函数)
测试逻辑简单易懂:
-
实例化一个存储int类型的无锁队列
LockFreeQueue<int> q; -
调用
enqueue入队两个整数(10、20); -
调用
dequeue出队,判断出队是否成功,若成功则打印出队数据; -
运行后输出两个出队数据,验证队列功能正常。