JMM + CAS + 伪共享:把 Java 热路径抖动打平

JMM + CAS + 伪共享:把 Java 热路径抖动打平

在高并发下,很多延迟尖峰来自三件事:可见性、竞争、以及缓存行抖动。下面给出能直接落地的最小组合拳。

1) JMM 可见性与有序性

volatile 保障可见性与部分有序,但保证复合操作原子性。读多写少可用 volatile 标志位;状态迁移/计数需原子语义或锁。

class Flags {
  volatile boolean started; // 可见但非原子复合
}

2) 无锁原子:VarHandle / LongAdder

JDK9+ 推荐 VarHandle,就地 CAS,避免额外对象开销;高冲突计数用 LongAdder 分段摊薄。

import java.lang.invoke.*;
class Counter {
  volatile long v;
  private static final VarHandle VH;
  static {
    try { VH = MethodHandles.lookup().findVarHandle(Counter.class, "v", long.class); }
    catch (Exception e) { throw new Error(e); }
  }
  void inc() {
    long p;
    do { p = (long) VH.getOpaque(this); } while (!VH.compareAndSet(this, p, p + 1));
  }
}
import java.util.concurrent.atomic.LongAdder;
class Adder {
  private final LongAdder adder = new LongAdder();
  void inc(){ adder.increment(); }
  long sum(){ return adder.sum(); }
}

3) 伪共享与缓存行对齐

热点多线程写入同一缓存行会频繁失效,导致“抖动”。JDK15+ 可用 @Contended(需 -XX:-RestrictContended)或手工填充。

import jdk.internal.vm.annotation.Contended;
class Slots {
  @Contended("x") volatile long x;
  @Contended("y") volatile long y;
}

手工填充示例:

class Padded {
  volatile long value;
  long p1,p2,p3,p4,p5,p6,p7; // 伪字段占位,隔离缓存行
}

4) 自旋与退避

CAS 失败时指数退避能显著降低总争用成本:

void spinInc(VarHandle vh, Object o) {
  int backoff = 1;
  for(;;){
    long p = (long) vh.getOpaque(o);
    if (vh.compareAndSet(o, p, p+1)) return;
    Thread.onSpinWait();
    if ((backoff = Math.min(backoff<<1, 256)) > 1) LockSupport.parkNanos(backoff);
  }
}

5) 基准与观察

  • 用 JMH 逐步替换:AtomicLong -> VarHandle -> LongAdder,观察 p95/p99。
  • 将“每次更新跨 CPU 核”的字段拆分到不同对象,减少跨 NUMA 迁移。
  • 读路径尽量无锁:配置快照用 volatile 指针+不可变对象,写时整体替换引用。

小结:在读多写少且线程数≥CPU 核心的场景下,首选“不可变配置 + volatile 引用 + LongAdder 统计”;写多或强一致路径用 VarHandle + 退避;任何共享计数/标志先排查伪共享,再谈算法。

评论 0