伪共享是因变量内存布局过近导致的性能问题:当多线程修改同一缓存行(主流64字节)内独立变量时,触发mesi协议频繁失效与重载,造成吞吐量断崖式下降;java中可用@contended注解或手动填充等方案隔离字段以消除竞争。

伪共享不是代码写错了,而是变量在内存里“站得太近”——两个线程各自改自己的变量,却因它们落在同一缓存行里,被 CPU 当成“同一个地盘”反复抢夺,结果性能断崖式下跌。
缓存行是性能竞争的底层单位
CPU 不按变量读写内存,而是按缓存行(Cache Line)操作,主流大小为 64 字节。一次加载,整块进 L1 缓存;一次修改,整行标记为脏并触发 MESI 协议同步。这意味着:
- 即使只改一个
long(8 字节),CPU 也会把前后共 64 字节一起搬进缓存 - 若变量 A 和 B 相邻且都在这 64 字节内,线程 1 改 A → 线程 2 的缓存行失效 → 线程 2 读 B 就得重新从主存加载
- 高频更新时,“改→失效→重载→再改”形成恶性循环,吞吐量可能下降 50%~700%
Java 中 @Contended 是专治伪共享的注解
@sun.misc.Contended(JDK 8 引入,需开启 JVM 参数 -XX:-RestrictContended)可强制将带该注解的字段隔离到独立缓存行,避免与其他字段挤在同一行。
- 作用对象:类字段(不支持局部变量或数组元素)
- 生效前提:必须配合 JVM 启动参数
-XX:+UnlockExperimentalVMOptions -XX:-RestrictContended - 典型用法:仅对高频独写字段使用,例如并发计数器、状态标志、自旋锁字段
示例:
public class PaddedCounter {
@sun.misc.Contended
volatile long counter1;
@sun.misc.Contended
volatile long counter2;
}
此时 JVM 会在 counter1 前后自动填充约 64 字节空隙,确保它独占一行;counter2 同理。无需手动算偏移、写 padding 数组。
比 @Contended 更通用的实践策略
@Contended 虽简洁,但有局限(如非 public 类不生效、部分 JVM 版本兼容性弱)。生产中常组合使用以下方式:
-
结构体填充(Padding):用
long或byte[64]显式隔开热字段,稳定可控,零运行时开销 -
按访问频率分组布局:把频繁写的字段集中放结构体开头,冷字段(如调试用的
lastUpdateTime)挪到末尾或单独结构体 -
线程局部化:用
ThreadLocal或 per-thread ring buffer,让每个线程操作自己内存段,彻底规避跨核争用 -
数组元素错位访问:若用数组存多个计数器,确保索引间隔 ≥ 8(64 ÷ 8),使
arr[0]和arr[1]不落入同行
识别伪共享不能靠猜,得靠工具
它不报错、不崩溃,只悄悄拖慢程序。关键信号包括:
- 多线程加速比远低于核心数(比如 8 核跑出不到 2 倍提速)
- CPU 使用率高,但实际 QPS/TPS 上不去
-
perf stat -e cache-misses,cache-references,instructions显示 cache-miss ratio > 10% - Intel VTune 或 async-profiler 报告大量
LLC-load-misses或remote-node-loads
最简验证法:对疑似字段加 64 字节填充,重测性能。明显变快,基本就是伪共享。










