欢迎光临
我们一直在努力

Java 伪共享问题详解:如何使用 @Contended 注解提升缓存行命中率

什么是伪共享(False Sharing)?

在高性能并发编程中,我们经常追求最小化锁的竞争,但即使我们避免了锁,也可能遇到一个棘手的性能瓶颈:伪共享(False Sharing)。

伪共享是由于CPU缓存机制引起的现象。现代CPU有多级缓存(L1, L2, L3),这些缓存不是按字节存储数据的,而是按“缓存行”(Cache Line)存储的,通常大小为64字节。

当两个或多个线程同时修改存储在同一缓存行但逻辑上不相关的变量时,就会发生伪共享。即使变量A和变量B是独立的,但由于它们恰好被放在同一个64字节的缓存行内,线程修改A时,整个缓存行都会被标记为“脏”(Dirty),并根据缓存一致性协议(如MESI)强制刷新或使其他核心上的该缓存行副本失效。这导致大量不必要的缓存同步流量,极大地降低了并发写入性能。

演示伪共享带来的性能损失

我们创建一个简单的基准测试,让多个线程分别修改一个数组中紧邻的元素。由于数组元素在内存中是连续存放的,紧邻的元素很可能落在同一个缓存行中。

准备代码:

我们定义一个用于存储值的类,并创建一个包含N个此类实例的数组,每个线程操作一个独立的索引。

class ValueHolder {
    public volatile long value = 0L;
}

public class FalseSharingDemo {
    private static final int NUM_THREADS = 4;
    private static final long ITERATIONS = 100000000L;
    private static final ValueHolder[] holders = new ValueHolder[NUM_THREADS];

    static {
        for (int i = 0; i < NUM_THREADS; i++) {
            holders[i] = new ValueHolder();
        }
    }

    public static long runTest() throws InterruptedException {
        Thread[] threads = new Thread[NUM_THREADS];

        long startTime = System.nanoTime();
        for (int i = 0; i < NUM_THREADS; i++) {
            final int index = i;
            threads[i] = new Thread(() -> {
                long localIterations = ITERATIONS;
                while (localIterations-- > 0) {
                    holders[index].value = localIterations; // 竞争写入
                }
            });
            threads[i].start();
        }

        for (Thread t : threads) {
            t.join();
        }
        return System.nanoTime() - startTime;
    }

    public static void main(String[] args) throws InterruptedException {
        // 预热 (JVM JIT)
        for (int i = 0; i < 5; i++) {
            runTest();
        }

        long duration = runTest();
        System.out.println("启用伪共享时耗时 (纳秒): " + duration);
    }
}

在我的测试环境中,这段代码可能耗时 400 毫秒到 600 毫秒。

使用 @Contended 解决伪共享问题

Java 8 引入了 @Contended 注解(位于 sun.misc 包,或者在较新版本中位于 jdk.internal.vm.annotation),用于解决伪共享问题。

当一个字段或一个类被 @Contended 注解修饰时,JVM 会自动在对象布局中插入额外的填充(Padding)字节,确保被注解的字段或整个对象不会与其它竞争性字段共享同一个缓存行。

注意: 默认情况下,@Contended 注解是受限制的(restricted),只有核心 JDK 类库才能使用。若要在自己的应用中使用,必须在启动 JVM 时添加参数:-XX:-RestrictContended

启用 @Contended 的修复代码:

我们修改 ValueHolder 类,使用 @Contended 注解。

// 需要在JVM启动参数中添加 -XX:-RestrictContended
@sun.misc.Contended
class PaddedValueHolder {
    public volatile long value = 0L;
}

public class FalseSharingFixDemo {
    private static final int NUM_THREADS = 4;
    private static final long ITERATIONS = 100000000L;
    private static final PaddedValueHolder[] holders = new PaddedValueHolder[NUM_THREADS];

    static {
        for (int i = 0; i < NUM_THREADS; i++) {
            holders[i] = new PaddedValueHolder();
        }
    }

    // ... runTest 方法与 FalseSharingDemo 相同 ...

    public static void main(String[] args) throws InterruptedException {
        // 运行测试时,请务必设置 JVM 参数:-XX:-RestrictContended

        // 预热
        for (int i = 0; i < 5; i++) {
            runTest();
        }

        long duration = runTest();
        System.out.println("禁用伪共享时耗时 (纳秒): " + duration);
    }

    // 简化:这里省略了 runTest 的重复定义,假设它与上一个示例相同,只是使用了 PaddedValueHolder 数组。
}

结果对比

启动 JVM 时加上 -XX:-RestrictContended 参数运行修正后的代码。你会发现执行时间大幅缩短。在我的测试环境中,修正后的代码可能仅耗时 150 毫秒到 250 毫秒,性能提升了近一倍甚至更多。

场景 JVM 参数 典型耗时(毫秒) 性能影响
伪共享(无 @Contended) 400-600
修复(使用 @Contended) -XX:-RestrictContended 150-250

总结

@Contended 注解是解决 Java 并发编程中伪共享问题的有效工具。它通过在对象字段周围插入填充字节,强制将竞争性变量分离到不同的缓存行中,从而消除不必要的缓存同步开销,显著提升在高并发写入场景下的性能。但请记住,使用此注解需要额外的 JVM 启动参数,并且填充本身会增加内存消耗,因此应谨慎用于确认存在伪共享瓶颈的场景。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Java 伪共享问题详解:如何使用 @Contended 注解提升缓存行命中率
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址