什么是伪共享?
在并发编程中,我们常听到一个词叫'伪共享'(False Sharing)。听起来有点绕,但实际影响很直接。简单来说,当多个线程同时修改同一个缓存行里的不同变量时,由于 CPU 缓存机制的限制,会导致性能显著下降。
为什么会发生?
CPU 从内存读取数据时,并不是按字节或单个变量进行的,而是以'缓存行'(Cache Line)为单位。通常一行是 64 字节。即使你的代码只访问了其中一个变量,整行数据都会被加载到缓存中。
这就埋下了隐患:如果两个线程分别操作位于同一缓存行的不同变量,虽然逻辑上它们互不干扰,但在硬件层面,它们共享了同一条缓存通道。当一个线程修改了该缓存行,另一个线程的缓存副本就会失效,必须重新从内存或其他核心获取最新数据。这种频繁的缓存一致性校验和同步,就是伪共享带来的性能损耗。
怎么解决?
既然知道了原因,解决方案也就清晰了。核心思路是让这些变量尽可能落在不同的缓存行里。
1. 手动填充(Padding)
这是最底层的做法。在定义变量时,预留足够的空间,确保每个变量占据独立的缓存行。比如,假设缓存行是 64 字节,你可以声明一个数组来填充剩余空间。
public class FalseSharing {
// 每个 long 占 8 字节,需要填充 7 个 long 才能凑够 64 字节
public static final int CACHE_LINE_SIZE = 64;
public static class Padding {
public volatile long p1, p2, p3, p4, p5, p6, p7; // 填充
public volatile long value; // 实际数据
public volatile long q1, q2, q3, q4, q5, q6, q7; // 填充
}
}
这种方式虽然有效,但代码可读性较差,而且依赖具体的架构参数。
2. 使用 @Contended 注解
JDK 8 引入了解决这个问题的利器——@sun.misc.Contended(后来移到了 jdk.internal.misc 包,但在标准库中通常通过 JVM 参数支持)。不过更推荐的方式是使用 @Contended 注解(需配合 -XX:-RestrictContended 等参数,或者直接使用 OpenJDK 提供的实现)。
在实际开发中,如果你使用的是较新的 JDK 版本,可以直接利用 @Contended 让 JVM 自动处理填充。这比手写 padding 要安全得多。
sun.misc.Contended;
{
count1;
count2;
}

