False Sharing(伪共享)是并发编程中的一个性能问题,它发生在多个线程访问并修改相互独立的变量时,这些变量恰好位于同一个缓存行(Cache Line)内。现代CPU为了提高访问内存的速度,会将内存分成一系列的缓存行,通常大小为64字节,并且以缓存行为单位将数据从主存(RAM)加载到CPU缓存中。当一个线程修改了一个缓存行内的数据时,处于同一缓存行的其他数据也会被标记为无效,导致其他线程在访问自己的变量时不得不重新从主存中加载整个缓存行,即使它们只是想要读取自己的独立变量。
举个例子
考虑以下Java代码,其中模拟了两个线程并发更新两个独立变量的情况:
public class FalseSharingExample implements Runnable {
public final static int NUM_THREADS = 2; // 线程数
public final static long ITERATIONS = 500L * 1000L * 1000L;
private final int arrayIndex;
private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
static {
for (int i = 0; i < longs.length; i++) {
longs[i] = new VolatileLong();
}
}
public FalseSharingExample(final int arrayIndex) {
this.arrayIndex = arrayIndex;
}
public static void main(final String[] args) throws Exception {
final long start = System.nanoTime();
runTest();
System.out.println("Duration = " + (System.nanoTime() - start));
}
private static void runTest() throws InterruptedException {
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new FalseSharingExample(i));
}
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
}
public void run() {
long i = ITERATIONS + 1;
while (0 != --i) {
longs[arrayIndex].value = i;
}
}
public final static class VolatileLong {
public volatile long value = 0L;
}
}
在这个例子中,有两个线程分别更新数组longs
中的两个独立元素。由于VolatileLong
对象很小,它们很可能被放置在同一个缓存行中。这意味着当一个线程更新它的value
变量时,可能会导致另一个线程的缓存行无效,因此增加了不必要的内存访问延迟。
解决伪共享
解决伪共享的一种常见方法是通过增加填充(Padding)来强制每个被频繁写入的变量都位于不同的缓存行中。Java 8引入了@Contended
注解,但默认是禁用的,您需要在JVM启动时通过-XX:-RestrictContended
来启用它。下面是一个增加了填充以避免伪共享的示例:
public final static class PaddedVolatileLong {
// 增加填充
public volatile long p1, p2, p3, p4, p5, p6 = 7L;
public volatile long value = 0L;
// 对齐到另一个缓存行,避免伪共享
public volatile long q1, q2, q3, q4, q5, q6 = 7L;
}
这种方法通过增加一些无用的变量p1
到p6
和q1
到q6
,确保value
前后有足够的空间将它与其他频繁写入的变量隔开,从而避免它们落在同一个缓存行中。
总结
伪共享是一个微妙但重要的性能问题,它可能导致并发程序的性能显著下降。通过合理的设计和一些高级特性(如Java的@Contended
注解或者手动填充),可以减轻甚至避免伪共享带来的影响。理解并发程序中的缓存行为对于编写高效的多线程代码至关重要。