news 2026/9/3 0:44:51

无锁队列 (Lock-Free) 实战:手撸一个比 BlockingQueue 快 10 倍的环形缓冲

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
无锁队列 (Lock-Free) 实战:手撸一个比 BlockingQueue 快 10 倍的环形缓冲

标签:#HighConcurrency #Java #Disruptor #CAS #Performance #DataStructure


📉 前言:锁的代价

BlockingQueue的本质是“悲观锁”
当一个线程想要入队时,它必须先拿到锁。如果锁被别人拿了,它就得挂起(Context Switch),进入内核态等待。这个过程对于 CPU 来说,简直是漫长的“世纪等待”。

无锁(Lock-Free)的本质是“乐观锁” (CAS)
线程说:“我猜现在没人改这个变量,我试着改一下。如果改成功了最好,改不成功我再重试。”
全程在用户态运行,没有线程挂起,CPU 满负荷运转。


🧠 一、 核心架构:RingBuffer 与 序号 (Sequence)

我们抛弃链表(LinkedList),因为链表的节点在内存中是分散的,对 CPU Cache 极其不友好。
我们使用数组(Array)实现环形缓冲。

设计难点:
多线程环境下,怎么知道哪个格子是空的?哪个格子有数据?
Disruptor 的解法:使用一个单调递增的Sequence(序号)。

逻辑图解 (Mermaid):

RingBuffer (Size=8)

1. CAS 获取写位置 Seq=2
2. CAS 获取读位置 Seq=0

0: Data A

1: Data B

2: Empty

3: Empty

4: Empty

5: Empty

6: Empty

7: Empty

Producer

Consumer

位运算优化: Index = Seq & Size-1


🛠️ 二、 这里的“黑科技”:解决伪共享 (False Sharing)

这是本篇最硬核的知识点。
CPU 读取内存不是一个字节一个字节读的,而是按“缓存行 (Cache Line)”读的,通常是 64 字节。

如果你的Head指针和Tail指针挨得太近(在同一个缓存行里):

  1. 核心 A 改了Head
  2. 核心 B 想改Tail
  3. 因为Head变了,整个缓存行失效。核心 B 必须重新从主存拉取数据。
    这就是“伪共享”,它会严重拖慢多核 CPU 的性能。

解决方案:缓存行填充 (Padding)。
我们在变量前后强行塞入 7 个long类型(7 * 8 = 56 字节),确保关键变量独占一行。

// 伪共享填充示例classPaddedAtomicLongextendsAtomicLong{// 前方填充publicvolatilelongp1,p2,p3,p4,p5,p6,p7=7L;// 真正的值在父类 AtomicLong 中// 后方填充publicvolatilelongq1,q2,q3,q4,q5,q6,q7=7L;}

💻 三、 代码实战:MPMC (多生产多消费) 无锁队列

这是一个简化版的 Dmitry Vyukov 算法实现(JCTools 也是基于此)。

1. 定义数据结构

我们需要一个数组来存数据,还需要一个额外的sequenceBuffer数组来标记每个槽位的“圈数”,用于判断该槽位是“空”还是“满”。

importjava.util.concurrent.atomic.AtomicLong;importjava.util.concurrent.atomic.AtomicReferenceArray;publicclassLockFreeRingBuffer<E>{privatefinalintcapacity;privatefinalintmask;// 存数据的数组privatefinalAtomicReferenceArray<E>buffer;// 存序号的数组 (用于解决竞态条件)privatefinalint[]sequenceBuffer;// 队头 (生产者索引) - 做了 Padding 优化privatefinalAtomicLonghead=newAtomicLong(0);// 队尾 (消费者索引) - 做了 Padding 优化privatefinalAtomicLongtail=newAtomicLong(0);publicLockFreeRingBuffer(intcapacity){// 容量必须是 2 的幂,方便位运算this.capacity=findNextPowerOfTwo(capacity);this.mask=this.capacity-1;this.buffer=newAtomicReferenceArray<>(this.capacity);this.sequenceBuffer=newint[this.capacity];// 初始化序号数组,0, 1, 2...for(inti=0;i<this.capacity;i++){sequenceBuffer[i]=i;}}// 辅助函数: 找最近的 2 的幂privateintfindNextPowerOfTwo(intn){return1<<(32-Integer.numberOfLeadingZeros(n-1));}}
2. 入队 (Offer) - 生产者的艺术

这里没有synchronized,只有CAS和自旋。

publicbooleanoffer(Ee){longcurrentHead;intcycle;// 自旋 (死循环重试)do{currentHead=head.get();intindex=(int)(currentHead&mask);intseq=sequenceBuffer[index];// 计算当前位置的“圈数”差值// 如果 seq == currentHead,说明这个坑位是空的,且正好轮到我intdif=seq-(int)currentHead;if(dif==0){// 尝试用 CAS 抢占这个位置,把 head + 1if(head.compareAndSet(currentHead,currentHead+1)){// 抢到了!放数据buffer.set(index,e);// 更新 sequence,标记为“已满”,让消费者可见// +1 表示数据已写入,等待消费sequenceBuffer[index]=(int)(currentHead+1);returntrue;}}elseif(dif<0){// dif < 0 说明队列满了 (seq 落后于 head)// 简单的策略:返回 false,或者你可以选择 Thread.yield() 让出 CPUreturnfalse;}// else: dif > 0,说明被别的线程抢先了,或者 index 计算异常,继续自旋}while(true);}
3. 出队 (Poll) - 消费者的竞速

逻辑与入队对称。

publicEpoll(){longcurrentTail;do{currentTail=tail.get();intindex=(int)(currentTail&mask);intseq=sequenceBuffer[index];// 计算差异// 如果 seq == currentTail + 1,说明有数据,且正好轮到我消费intdif=seq-(int)(currentTail+1);if(dif==0){// CAS 抢占if(tail.compareAndSet(currentTail,currentTail+1)){Ee=buffer.get(index);// 拿走数据后,把格子置空buffer.set(index,null);// 更新 sequence,标记为“空”,而且是“下一圈”的空// capacity 表示跳过一整圈sequenceBuffer[index]=(int)(currentTail+mask+1);returne;}}elseif(dif<0){// 队列空了returnnull;}}while(true);}

📊 四、 性能对比与总结

在 i7 处理器,4 线程并发读写的基准测试(JMH)中:

队列类型吞吐量 (ops/ms)延迟 (ns)
ArrayBlockingQueue~4,500~12,000
LockFreeRingBuffer~48,000~800

为什么快了 10 倍?

  1. 无锁竞争:消除了内核态切换的开销。
  2. 伪共享解决:Padding 让 CPU 缓存行利用率最大化。
  3. 位运算& mask%取模运算快得多。
  4. 预分配内存:RingBuffer 避免了链表节点的频繁 GC。

🎯 总结

手写无锁队列是理解并发编程皇冠上明珠的最佳途径。
虽然在生产环境中,我推荐你直接使用成熟的Disruptor库或JCTools(Netty 就在用它),但理解了CASPaddingMemory Barrier,你写出的代码将不仅是代码,而是艺术。

Next Step:
尝试给上面的代码加上@Contended注解(Java 8+)来替代手动的 Padding,并使用 JMH 跑个分,看看你的 CPU 会不会烫得冒烟!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 22:06:57

dify知识库索引异常处理全攻略(段落过长问题深度解析)

第一章&#xff1a;dify知识库索引异常处理全攻略&#xff08;段落过长问题深度解析&#xff09; 在使用 Dify 构建智能应用时&#xff0c;知识库的索引质量直接影响检索效果。当文档中存在段落过长的情况&#xff0c;可能导致语义切分失效、向量嵌入不准确&#xff0c;进而引发…

作者头像 李华
网站建设 2026/9/2 21:26:26

零基础想学黑客?推荐你了解一下Kali Linux!(建议收藏)

最近好多朋友问我&#xff1a;不会编程&#xff0c; 英语也不好&#xff0c;dos命令也记不住&#xff0c;能学习黑客技术么&#xff1f; 我可以明确告诉大家&#xff0c;可以的&#xff01; 相信每一个少年心中&#xff0c;曾经都有过一个黑客梦&#xff01; 有人觉得黑客霸…

作者头像 李华
网站建设 2026/9/2 20:40:14

Spring Boot调试还在靠“玄学”?IntelliJ这个隐藏插件让你直接透视!

通过 Spring Debugger 插件&#xff0c;IntelliJ IDEA 为标准调试器添加了 Spring 相关洞察&#xff0c;简化应用故障排查。1. 简介 Spring Boot 通过少量依赖和最小配置&#xff0c;使构建强大应用变得很容易。只需几行代码&#xff0c;我们就可以设置 HTTP 端点、连接数据库…

作者头像 李华