最近公司降薪裁员,越来越生气,诶
在分布式锁的讨论中,我们还在纠结"红绿灯"(Lock)的等待时间。但在高频交易 (HFT) 、电信网关 、实时推荐系统 等极致性能场景下,任何毫秒级的停顿都是不可接受的。
传统的 synchronized 或 ReentrantLock 会导致线程挂起(Context Switch),消耗 CPU 周期去保存/恢复寄存器状态。
无锁编程 (Lock-Free) 的核心思想是:彻底抛弃"红绿灯"。
- 比喻:就像 F1 赛车进站。传统锁是"停车换挡"(线程阻塞);无锁编程是"无缝换挡"(CAS 原子操作 + 环形缓冲区),车手(线程)从未松开油门,只是通过精密的跑道设计(内存布局)和避让规则(算法)完成了超车。
这一领域的巅峰之作是 LMAX Disruptor ,它能达到单机每秒 600 万+ 的消息吞吐量,延迟在微秒级。
第一部分:核心基石------CAS 与 伪共享消除
看吧我就说cas很厉害吧
1. 什么是 CAS (Compare-And-Swap)?
无锁编程不靠"排队",靠的是"乐观重试"。
CPU 提供了一条原子指令 CMPXCHG。
-
逻辑 :
if (内存值 == 预期值) { 内存值 = 新值; return true; } else { return false; } -
特点:硬件层面保证原子性,无需操作系统介入,不会导致线程挂起。
-
缺点:如果竞争极其激烈,线程会一直自旋(Spin),空转 CPU(称为"忙等待")。但在低竞争或设计良好的系统中,这比上下文切换快得多。
public final long getAndIncrement() {
long next;
do {
// 1. 读取当前值
long current = unsafe.getLongVolatile(this, valueOffset);
// 2. 计算新值
next = current + 1;
// 3. CAS 尝试更新:如果 current 没变过,就更新为 next
// 如果失败(被别人改了),循环重试
} while (!unsafe.compareAndSwapLong(this, valueOffset, current, next));
return next;
}
2. 致命陷阱:伪共享 (False Sharing)
这是无锁编程中最隐蔽的性能杀手,也是 Disruptor 优化的核心。
-
原理 :CPU 缓存不是按字节加载的,而是按 Cache Line (缓存行) 加载的(通常 64 字节)。
-
问题:
- 线程 A 修改变量
x(位于 Cache Line 1)。 - 线程 B 修改变量
y(也位于 Cache Line 1,因为x和y挨得太近)。 - 虽然
x和y逻辑无关,但 CPU 认为整个 Cache Line 被污染了。 - 后果 :CPU 核心间必须频繁同步缓存(MESI 协议),导致性能下降 10-100 倍!这就好比两辆 F1 赛车在跑道上因为靠太近,不得不频繁刹车避让。
- 线程 A 修改变量
-
解决方案:缓存行填充 (Padding)
在关键变量前后填充无用的字节,强制将其隔离到不同的 Cache Line 中。
// ❌ 错误示范:会发生伪共享
class WrongCounter {
public volatile long p1; // 线程 A 写
public volatile long p2; // 线程 B 写
// p1 和 p2 很可能在同一个 64 字节缓存行里
}// ✅ 正确示范 (JDK 8 手动填充)
class RightCounterJDK8 {
// 填充前 64 字节
public long p1, p2, p3, p4, p5, p6, p7;public volatile long value; // 目标变量,独占一行 // 填充后 64 字节 public long q1, q2, q3, q4, q5, q6, q7;}
// ✅ 正确示范 (JDK 9+ 注解优化)
import sun.misc.Contended;class RightCounterJDK9 {
@Contended // 自动添加填充,无需手写 p1...p7
public volatile long value;
}
第二部分:Disruptor 架构深度解析
Disruptor 之所以快,是因为它把上述理论发挥到了极致。它不仅仅是一个队列,而是一个事件处理框架。
1. 环形缓冲区 (Ring Buffer)
- 设计 :预分配一个固定大小的数组(必须是 2n2n ,以便用位运算代替取模
%)。 - 优势 :
- 无 GC 压力 :对象预先创建好,复用,没有频繁的
new和垃圾回收。 - 内存局部性:数据在内存中连续,CPU 缓存命中率极高。
- 无锁入队 :生产者只需要 CAS 更新一个
cursor指针。
- 无 GC 压力 :对象预先创建好,复用,没有频繁的
2. 单生产者 vs 多生产者
- 单生产者 (Single Producer) :完全无锁!只有一个线程写
cursor,消费者读cursor。连 CAS 都不需要,直接++。 - 多生产者 (Multi Producer) :使用 CAS 竞争
cursor。
3. 等待策略 (Wait Strategy)
消费者如何发现新数据?
- BlockingWaitStrategy :用
LockSupport.park(),省 CPU 但延迟高(类似传统锁)。 - BusySpinWaitStrategy :死循环
while,延迟最低,但吃满一个 CPU 核(F1 赛车模式,适合独享核心)。 - YieldingWaitStrategy :
Thread.yield(),折中方案。
业务使用实例
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.4</version> <!-- 建议使用最新稳定版 -->
</dependency>
案例一:单生产者 - 单消费者 (日志异步写入)
Web 服务器接收请求,生成日志。为了不阻塞主线程,使用 Disruptor 将日志对象传递给后台线程写入磁盘。
无锁(Single Producer 不需要 CAS),性能最高。
//这里没有 new 操作,对象由工厂预创建。定义事件对象 (Event)
public class LogEvent {
private long sequence;
private String message;
private long timestamp;
// Setter/Getter (Disruptor 直接操作字段,通常不需要 getter/setter 的开销,但为了规范保留)
public void setValues(long sequence, String message, long timestamp) {
this.sequence = sequence;
this.message = message;
this.timestamp = timestamp;
}
public String getMessage() { return message; }
public long getTimestamp() { return timestamp; }
}
//负责初始化环形缓冲区中的对象,只在启动时运行一次
import com.lmax.disruptor.EventFactory;
public class LogEventFactory implements EventFactory<LogEvent> {
@Override
public LogEvent newInstance() {
return new LogEvent();
}
}
//定义事件处理器 (EventHandler) 消费者的核心逻辑,相当于 run() 方法。
import com.lmax.disruptor.EventHandler;
public class LogEventProcessor implements EventHandler<LogEvent> {
@Override
public void onEvent(LogEvent event, long sequence, boolean endOfBatch) throws Exception {
// 模拟耗时操作:写入文件/网络
// 实际生产中这里会调用 Logger.append()
System.out.println("[Consumer] Thread: " + Thread.currentThread().getName()
+ " | Seq: " + sequence
+ " | Msg: " + event.getMessage()
+ " | Time: " + event.getTimestamp());
// 重要:如果是长生命周期对象,处理完后建议清空引用防止内存泄漏(虽然环形缓冲会覆盖,但大对象建议显式清理)
// event.setMessage(null);
}
}
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.util.DaemonThreadFactory;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class SingleProducerDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 定义缓冲区大小 (必须是 2 的幂,例如 1024, 4096)
int bufferSize = 1024;
// 2. 创建 Executor (用于启动消费者线程)
Executor executor = Executors.newFixedThreadPool(1, DaemonThreadFactory.INSTANCE);
// 3. 构建 Disruptor
// 参数:工厂,缓冲区大小,线程工厂,生产者类型(单/多), 等待策略
Disruptor<LogEvent> disruptor = new Disruptor<>(
new LogEventFactory(),
bufferSize,
executor,
com.lmax.disruptor.ProducerType.SINGLE, // 关键:单生产者优化
com.lmax.disruptor.WaitStrategy.BLOCKING // 生产环境常用 Blocking 或 Yielding
);
// 4. 绑定消费者
disruptor.handleEventsWith(new LogEventProcessor());
// 5. 启动
disruptor.start();
// 6. 获取发布器 (Publisher)
var ringBuffer = disruptor.getRingBuffer();
// 7. 模拟生产数据
for (long i = 0; i < 10; i++) {
final long seq = i;
// next() 返回序列号,并预留槽位
long sequence = ringBuffer.next();
try {
// 在预留的槽位中填充数据 (零拷贝,直接修改数组中的对象)
LogEvent event = ringBuffer.get(sequence);
event.setValues(sequence, "Log Message #" + i, System.currentTimeMillis());
} finally {
// 通知消费者数据已准备好 (释放屏障)
ringBuffer.publish(sequence);
}
}
Thread.sleep(1000); // 等待消费完成
disruptor.shutdown();
}
}
案例二:多生产者 - 单消费者 (订单汇聚)
场景 :多个 Web 线程同时接收订单,需要汇聚到一个线程中进行"库存扣减"或"风控检查"。
特点 :生产者之间需要 CAS 竞争序列号,会有轻微性能损耗,但依然远快于 BlockingQueue
// ... 前面的 Event 和 Handler 定义不变 ...
public class MultiProducerDemo {
public static void main(String[] args) throws InterruptedException {
int bufferSize = 4096;
Executor executor = Executors.newFixedThreadPool(1, DaemonThreadFactory.INSTANCE);
Disruptor<LogEvent> disruptor = new Disruptor<>(
new LogEventFactory(),
bufferSize,
executor,
com.lmax.disruptor.ProducerType.MULTI, // 关键:多生产者,内部使用 CAS
com.lmax.disruptor.WaitStrategy.YIELDING // 推荐:Yielding 平衡延迟和 CPU
);
disruptor.handleEventsWith(new LogEventProcessor());
disruptor.start();
var ringBuffer = disruptor.getRingBuffer();
// 模拟 5 个生产者线程
int producerCount = 5;
Thread[] threads = new Thread[producerCount];
for (int i = 0; i < producerCount; i++) {
final int producerId = i;
threads[i] = new Thread(() -> {
for (long j = 0; j < 20; j++) {
long sequence = ringBuffer.next(); // 内部会自动处理 CAS 竞争
try {
LogEvent event = ringBuffer.get(sequence);
event.setValues(sequence, "Order from Producer-" + producerId + " #" + j, System.currentTimeMillis());
} finally {
ringBuffer.publish(sequence);
}
// 模拟一点生产间隔
try { Thread.sleep(1); } catch (Exception e) {}
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
Thread.sleep(1000);
disruptor.shutdown();
}
}
案例三:复杂依赖链 (Diamond 模式 - 金融风控)
场景:一个交易事件需要经过三个步骤:
-
A: 基础校验 (Format Check)
-
B: 风控规则 (Risk Check) ------ 依赖 A
-
C: 持久化 (DB Save) ------ 依赖 A
-
D: 发送通知 (Notify) ------ 依赖 B 和 C (必须等 B 和 C 都做完才能做 D)
[Start] | (Handler A) / \(Handler B) (Handler C)
\ /
(Handler D)import com.lmax.disruptor.dsl.EventHandlerGroup;
public class DiamondPatternDemo {
public static void main(String[] args) {
int bufferSize = 1024;
Executor executor = Executors.newCachedThreadPool(DaemonThreadFactory.INSTANCE);Disruptor<LogEvent> disruptor = new Disruptor<>( new LogEventFactory(), bufferSize, executor, com.lmax.disruptor.ProducerType.SINGLE, com.lmax.disruptor.WaitStrategy.BUSY_SPIN // 极致性能用 BusySpin ); // 1. 定义所有 Handler LogEventProcessor handlerA = new LogEventProcessor() { @Override public void onEvent(LogEvent e, long s, boolean eob) { System.out.println("Step A: 基础校验完成 -> " + e.getMessage()); } }; LogEventProcessor handlerB = new LogEventProcessor() { @Override public void onEvent(LogEvent e, long s, boolean eob) { System.out.println("Step B: 风控检查完成 -> " + e.getMessage()); } }; LogEventProcessor handlerC = new LogEventProcessor() { @Override public void onEvent(LogEvent e, long s, boolean eob) { System.out.println("Step C: 数据库保存完成 -> " + e.getMessage()); } }; LogEventProcessor handlerD = new LogEventProcessor() { @Override public void onEvent(LogEvent e, long s, boolean eob) { System.out.println("Step D: 【最终】通知发送 (依赖 B+C) -> " + e.getMessage()); } }; // 2. 编排依赖关系 (核心语法) // handleEventsWith(handlerA): 第一步是 A // then(handlerB, handlerC): A 完成后,B 和 C 并行执行 // then(handlerD): B 和 C 都完成后,才执行 D EventHandlerGroup<LogEvent> groupA = disruptor.handleEventsWith(handlerA); EventHandlerGroup<LogEvent> groupBC = groupA.then(handlerB, handlerC); groupBC.then(handlerD); disruptor.start(); // 发布一个事件测试 var ringBuffer = disruptor.getRingBuffer(); long seq = ringBuffer.next(); try { ringBuffer.get(seq).setValues(seq, "Trade-ID-999", System.currentTimeMillis()); } finally { ringBuffer.publish(seq); } // 保持运行观察输出 try { Thread.sleep(2000); } catch (Exception e) {} disruptor.shutdown(); }}
输出顺序保证 :
你会看到 A 最先打印,然后 B 和 C 乱序打印(取决于线程调度),最后 D 一定在 B 和 C 之后打印。Disruptor 内部的 Barrier (屏障) 机制自动处理了这些复杂的等待逻辑,无需你写一行 wait/notify 代码。
工作总结建议
-
事件对象复用:
- Do : 在
EventFactory中创建对象,在onEvent中修改字段。 - Don't : 在
onEvent或生产者循环中new LogEvent()。这会引入 GC,破坏无锁优势。
- Do : 在
-
选择正确的 WaitStrategy:
- BusySpin: 延迟最低 (<1μs),但独占 CPU 核。适合独享服务器的 HFT。
- Yielding: 延迟低,CPU 占用中等。适合大多数高性能场景。
- Blocking: 延迟较高,CPU 占用低。适合业务逻辑本身很慢(如写磁盘、调 RPC)的场景,避免空转浪费电。
-
批量处理 (Batch):
- 在
onEvent中,可以一次性拉取一批事件处理,减少方法调用开销。 - Disruptor 提供了
BatchEventProcessor,或者你可以手动在onEvent里判断endOfBatch标志位来优化。
- 在
-
异常处理:
onEvent抛出的异常会导致消费者线程终止!- 必须 在
onEvent内部try-catch所有业务异常,并记录日志,确保线程存活。 - 或者实现
ExceptionHandler接口注册到 Disruptor 中。
-
关于内存泄漏:
- 如果 Event 中包含大对象(如 byte[] 图片数据),在处理完毕后,最好在 Event 中将该引用置为
null,或者在 RingBuffer 覆盖该槽位前确保不再被引用。虽然 RingBuffer 是循环覆盖的,但如果有外部引用持有 Event,GC 就无法回收。
- 如果 Event 中包含大对象(如 byte[] 图片数据),在处理完毕后,最好在 Event 中将该引用置为
第三部分:源码级深度剖析 (简化版)
我们手写一个极简版的单生产者 RingBuffer ,展示如何利用位运算 和CAS实现无锁。
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.LongConsumer;
public class SimpleRingBuffer<T> {
private final T[] buffer;
private final int bufferSize;
private final int mask; // 用于位运算:index = sequence & mask
private final AtomicLong cursor; // 生产者的游标,volatile + CAS
@SuppressWarnings("unchecked")
public SimpleRingBuffer(int size) {
// 1. 大小必须是 2 的幂
if ((size & (size - 1)) != 0) {
throw new IllegalArgumentException("Size must be power of 2");
}
this.bufferSize = size;
this.mask = size - 1; // 例如 size=8, mask=7 (0111)
this.buffer = (T[]) new Object[size];
this.cursor = new AtomicLong(-1); // 初始为 -1,表示空
// 2. 预填充对象 (避免运行时 new)
for (int i = 0; i < size; i++) {
buffer[i] = null; // 实际使用中这里会实例化具体 Event 对象
}
}
/**
* 生产者:发布事件 (无锁核心)
*/
public long publish(T event) {
// 1. 获取下一个序列号 (CAS 自旋)
long nextSequence;
while (true) {
long currentSequence = cursor.get();
nextSequence = currentSequence + 1;
// CAS 更新 cursor
if (cursor.compareAndSet(currentSequence, nextSequence)) {
break;
}
// 失败则重试 (自旋)
Thread.onSpinWait(); // JDK9+ 提示 CPU 这是自旋
}
// 2. 计算数组索引 (位运算代替取模,极快)
// 例:sequence=8, mask=7 -> 8 & 7 = 0 (回到数组开头)
long index = nextSequence & mask;
// 3. 写入数据 (此时只有当前线程拥有该槽位的写权)
buffer[(int) index] = event;
return nextSequence;
}
/**
* 消费者:读取事件
* @param consumer 处理逻辑
* @param lastConsumedSequence 消费者上次处理到的位置
*/
public long consume(LongConsumer consumer, long lastConsumedSequence) {
long currentCursor = cursor.get(); // 读取生产者进度 (volatile 读)
// 如果有新数据
if (currentCursor > lastConsumedSequence) {
long nextToProcess = lastConsumedSequence + 1;
// 简单起见,这里一次只处理一个。实际 Disruptor 可以批量处理
long index = nextToProcess & mask;
T event = buffer[(int) index];
if (event != null) {
// 业务逻辑
consumer.accept(nextToProcess);
// 注意:真实场景中,处理完后可能需要清理引用防内存泄漏
// buffer[(int)index] = null;
return nextToProcess; // 返回新的消费进度
}
}
return lastConsumedSequence;
}
}
Disruptor 的高级技巧:预声明与列存储
真实的 Disruptor 不仅仅是上面的代码,它还做了更极致的优化:
- EventFactory:启动时一次性创建所有 Event 对象。
- 列式存储 (Column Storage) :
- 普通队列:
[ {id:1, val:A}, {id:2, val:B} ](对象数组)。访问val需要跳来跳去。 - Disruptor 模式:
ids: [1, 2, ...], vals: [A, B, ...](多个数组)。 - 优势 :CPU 预取指令 (Prefetching) 能一次性把一整行
vals加载到缓存,处理速度提升数倍。
- 普通队列:
第四部分:性能对比与应用场景
| 组件 | 吞吐量 (msgs/sec) | 平均延迟 | 原理 | 适用场景 |
|---|---|---|---|---|
| ArrayBlockingQueue | ~10 万 | ~50 μs | 锁 + 条件变量 | 一般业务解耦 |
| ConcurrentLinkedQueue | ~50 万 | ~20 μs | CAS 链表 (有 GC 压力) | 中等并发 |
| Disruptor (单产) | 600 万+ | < 1 μs | 环形数组 + 无锁 + 预分配 | 高频交易、日志收集 |
| Disruptor (多产) | ~300 万 | ~2 μs | 环形数组 + CAS 竞争 | 多源数据聚合 |
2. 什么时候使用 Disruptor?
- ✅ 高频交易 (HFT):纳秒级延迟要求,每一微秒都关乎金钱。
- ✅ 金融风控:实时计算每一笔交易的欺诈风险。
- ✅ 游戏服务器:帧同步、状态更新,要求极低延迟。
- ✅ 高性能日志框架:如 Log4j2 的 AsyncAppender 默认就是基于 Disruptor。
- ❌ 普通 Web
3. 潜在风险
- CPU 占用 :如果使用
BusySpin策略,它会占满一个 CPU 核心。在云环境中(共享 CPU)可能导致费用激增或被限流。 - 内存占用:预分配大数组,即使空闲也占用内存。
- 开发复杂度:需要理解序列号、屏障 (Barrier)、等待策略,调试困难。
总结:从"红绿灯"到"F1 赛道"
无锁编程是并发领域的"珠穆朗玛峰"。
- 传统锁:像是城市交通,依靠红绿灯(Lock)指挥,安全但慢,频繁启停。
- Disruptor :像是 F1 赛道。
- 环形缓冲区 = 封闭环形赛道:没有岔路,不用减速找路。
- 预分配内存 = 顶级燃油:随时待命,无需临时加油(GC)。
- 伪共享填充 = 车道隔离带:确保赛车(线程)互不干扰,全速飞驰。
- CAS = 无缝换挡:依靠精密的机械结构(CPU 指令)瞬间完成动力切换,无需踩离合器(挂起线程)。