在多线程并发编程中,很多性能问题并非来自锁竞争或算法复杂度,而是隐藏在CPU缓存层的伪共享。当多个线程分别修改位于同一个缓存行中的不同变量时,由于缓存一致性协议的作用,CPU核心之间会不断使对方的缓存行失效,导致频繁的缓存同步,严重时可使程序吞吐量下降一个数量级。

一、CPU缓存行与伪共享原理
现代处理器为了缓解内存与CPU之间的速度差距,引入了多级缓存结构,其中L1、L2通常为核私有,L3为共享。缓存与内存之间交换数据的最小单位称为缓存行(Cache Line),在x86架构下一般为64字节。也就是说,哪怕你只读取一个int变量,CPU也会把该变量所在连续的64字节一并加载进缓存。
当两个不同的线程运行在不同核心上,各自修改同一个缓存行内的不同变量(例如对象中的字段a和字段b),由于MESI等缓存一致性协议要求同一地址的数据在多个缓存中保持一致,一个核心修改后,另一个核心对应的整个缓存行会被标记为无效,必须重新从主存或更高级缓存加载。这种因共享缓存行而导致的相互牵制,就叫作伪共享(False Sharing),因为线程之间并没有逻辑上的共享数据,却因物理布局被迫同步。
1.1 一个直观的代码示例
下面这段代码模拟了两个线程分别修改自己专属计数器,但两个计数器在内存中紧邻,极易落入同一缓存行:
public class FalseSharingDemo {
// 两个volatile long,紧邻声明,很可能在同一缓存行
public volatile long counterA = 0L;
public volatile long counterB = 0L;
public static void main(String[] args) throws InterruptedException {
FalseSharingDemo demo = new FalseSharingDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
demo.counterA++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
demo.counterB++;
}
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("耗时: " + (System.currentTimeMillis() - start) + "ms");
}
}
在上面的代码中,counterA和counterB都是long类型,各占8字节,它们作为同一个对象的字段,在对象内存布局中连续存放,极大概率处于同一个64字节缓存行。当t1修改counterA、t2修改counterB时,两个核心的缓存行频繁互失效,实测耗时往往明显高于单线程顺序执行。
我们可以通过填充字节的方式手动将二者隔开,使它们分属不同缓存行,但这种方式依赖对象头大小和字段排列,不够优雅且易因JVM实现差异失效。更好的做法是使用JDK提供的标准机制。
二、@Contended注解的工作机制
从JDK 8开始,Java在sun.misc(后移至jdk.internal.vm.annotation)包中引入了@Contended注解,用于解决字段级伪共享。它的核心思想是:在被标注的字段前后插入padding(填充),或者在类级别将不同分组字段隔离到不同缓存行。
@Contended可以用在类上,也可以用在字段上。用在字段上时,JVM会在该字段前后加上足够多的填充字节,确保其独占一个缓存行;用在类上时,配合contended分组,可将不同组的字段放到不同的缓存行区域。需要注意的是,该注解默认仅对JDK内部类生效,用户代码必须使用JVM参数-XX:-RestrictContended(JDK 8)或确保开启相应支持,并在高版本JDK中通过--add-exports开放模块访问。
2.1 使用@Contended改造示例
我们将前面的demo用@Contended改写,让counterA和counterB各自独占缓存行:
import jdk.internal.vm.annotation.Contended;
public class ContendedDemo {
@Contended
public volatile long counterA = 0L;
@Contended
public volatile long counterB = 0L;
public static void main(String[] args) throws InterruptedException {
ContendedDemo demo = new ContendedDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
demo.counterA++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100_000_000; i++) {
demo.counterB++;
}
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("耗时: " + (System.currentTimeMillis() - start) + "ms");
}
}
在开启-XX:-RestrictContended的JVM中运行上述代码,counterA和counterB会被JVM安排到不同的缓存行,线程间不再因缓存行失效相互阻塞,执行时间通常能恢复到接近理论并行值。字段级@Contended在底层相当于替我们做了缓存行对齐的脏活。
如果是类级别使用,例如@Contended("group1")和@Contended("group2")标注不同字段,JVM会把同一组的字段紧凑放置,而不同组之间插入填充隔离,适合多个字段需分组共享缓存行的复杂结构。
三、优化策略与注意事项
虽然@Contended能显著提升高并发下的性能,但也不是银弹。插入填充意味着更多的内存占用,在对象实例极多的场景下会放大GC压力。因此应当只对真正被多线程频繁写入且相互独立的字段使用。
此外,伪共享还可能出现在数组元素上。例如一个long数组,相邻元素很容易同处一个缓存行,此时可借助填充对象或Java 9之后的VarHandle配合对齐方式缓解。对于大多数业务系统,先通过性能剖析工具(如perf、JMH)确认存在缓存行争用,再针对性使用@Contended,才是合理的工程做法。
3.1 使用JMH进行基准验证
下面是一段简化的JMH测试思路,用于对比有无@Contended的吞吐差异:
import org.openjdk.jmh.annotations.*;
import jdk.internal.vm.annotation.Contended;
import java.util.concurrent.TimeUnit;
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class CacheLineBenchmark {
@Contended
public volatile long x;
public volatile long y;
@Benchmark
public void writeX() {
x++;
}
@Benchmark
public void writeY() {
y++;
}
}
通过并发执行writeX和writeY,可以观察到当x使用@Contended隔离后,二者并行写的平均耗时明显低于未隔离状态。这从基准层面证实了伪共享的真实存在与注解优化的有效性。
总结来说,伪共享是底层硬件特性在高级语言中的隐性映射,理解CPU缓存行大小与一致性协议,善用@Contended并在必要时手动padding,才能让Java并发程序真正跑满多核性能。
False_SharingCPU缓存行Contended注解修改时间:2026-08-07 14:24:35