MpscLinkedQueue 是 Netty 所实现的一个高性能的**基于多生产者单消费者的无锁队列。**主要用于在多线程环境下进行高效的数据交换。其设计目标是支持多个生产者线程并发地入队操作,而只允许单个消费者线程执行出队操作。这种队列常见于
核心设计目标:MPSC 场景下的极致性能
适用场景:需要高吞吐、低延迟的场景,比如事件驱动架构、消息传递系统、异步任务处理等。
关键特性:
- 多生产者单消费者(MPSC):
多个线程可以安全地并发地向队列中插入元素(enqueue)。
只有一个线程可以从队列中取出元素(dequeue)。
- 无锁(Lock-Free):
使用原子操作(如CAS)而非传统锁机制来协调线程间的访问,避免了上下文切换和阻塞,提高了性能。
- 链表结构:
基于链表节点链接而成,可动态扩展,不受固定容量限制(除非受内存限制)。
- 高效率:
通过消除锁竞争和减小同步开销,实现高吞吐量和低延迟,特别适合高并发场景。
- 内存预分配/缓存友好:
某些实现会采用对象池或缓存行填充来优化性能,减少伪共享(false sharing)和GC压力。
现代 CPU 以 缓存行(Cache Line) 为单位缓存内存数据,常见大小为 64 字节。
当多个线程访问 同一缓存行内的不同变量 时,即使它们逻辑上无关,也会导致缓存同步冲突 ------ 这就是 伪共享(False Sharing)。
缓存行填充:通过在变量周围添加无意义的"填充字段",让每个易变变量独占一个缓存行。
实现原理
链表结构
java
// netty-common/src/main/java/io/netty/util/internal/mpsc/MpscLinkedQueue.java
public class MpscLinkedQueue<E> extends AbstractQueue<E> implements Queue<E>, Iterable<E> {
// 队列头节点(仅消费者可见)
private volatile Node consumerNode;
// 队列尾节点(生产者可见)
private volatile Node producerNode;
// 节点定义(内存对齐,防止伪共享)
static class Node {
static final Node[] CACHE_LINE_PADDING = new Node[128]; // 缓存行填充
volatile Object value; //存储实际的数据项
volatile Node next; //指向下一个节点的原子引用
static final Node[] CACHE_LINE_PADDING2 = new Node[128];
}
// 构造函数
public MpscLinkedQueue() {
Node node = new Node();
node.value = null;
producerNode = consumerNode = node;
}
}
入队操作
生产者线程执行以下步骤:
a. 创建新节点;
b. 获取当前尾节点(tail),尝试将 tail.next 设置为新节点(使用 CAS);
c. 如果成功,再尝试推进 tail 指针到新节点(这一步也可能由其他生产者完成);
d. 如果失败(说明有竞争),自旋重试或使用帮助机制(helping);
java
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final Node node = new Node();
node.value = e;
Node prev = producerNode.get();
Node curr;
do {
curr = prev.next.get();
if (curr != null) { //已经有别的线程成功地把节点链接到了 prev后面
// 帮助消费者推进 producerNode(避免旧生产者节点滞留)
prev = U.loadFence(prev.next.get());
continue;
}
node.next.set(curr); // 原子设置新节点的 next
} while (!prev.next.compareAndSet(curr, node));
// 原子更新 producerNode(仅当 prev 是当前尾节点)
U.putOrdered(producerNode, node);
return true;
}
其中,U.loadFence()/ putOrdered():都是用于控制 内存可见性与指令重排序 的工具方法。
loadFence():插入一个读屏障,在此指令之前的 所有读操作(load)必须在这个 fence 之前完成,之后的读不能重排到前面。从而确保能看到一个 最新、正确的引用值。
这是一种 "helping" 机制:当前生产者线程帮助其他线程推进
producerNode,避免因为某个线程挂起导致整个队列卡住。putOrdered():执行一个"宽松的写操作",不一定立即刷新到主存,但保证最终写入对其他线程可见。
出队操作
消费者线程执行以下步骤:
a. 检查 head 是否等于 tail(或者 head.next == null),如果是则队列为空;
b. 否则,从 head.next 节点取出 value;
c. 推进 head 指针到下一个节点(这通常是一种"懒惰"更新,即允许 head 滞后于实际消费点,依赖 GC 回收旧节点);
d. 返回 value;