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