Java 伪共享(False Sharing)

在主存中缓存是以cache line为单元存储的。cache line的长度是2的指数倍,一般为32到256之间。大部分cache line的长度为64 bytes。伪共享是指当不同线程修改在同一个cache line中的多个不相关变量时,造成性能下降的问题。在SMP系统中,在cache line中同时写入也是多线程并发执行的一个重要限制因素。

为保证多个线程可线型化执行,必须确保任何两个线程不会同时写同一个变量或同一个cache line。对同一个变量的操作可以在代码级别上进行追踪。对于cache line上的多变量写,我们需要知道内存布局或者通过工具来检测。比如,Intel VTune。这篇文章将解释Java对象在内存中是如何分布的,以及我们如何避免伪共享。

Java 伪共享(False Sharing)_第1张图片

上图所示为伪共享的示例。一个线程在Core1上运行,更新变量X,另一个线程在Core2上更新变量Y。但两个变量在同一个cache line上。每个线程都会竞争cache line的所有权,用于更新。一旦Core1获取权限,Core2对cache line将不起效,反之亦然。这将造成所有权的来回转换影响L3 cache的性能。当这些处理的Core被不同的socket占用,并且需要通过socket链接时,情况会更加恶化。

Hotspot JVM中,所有的对象都有两个字(非4bytes)的头。第一个是"mark"字,由24bits的hashcode和8bits的flags(用于锁的状态或被替换为锁对象),共32bits; 第二个字是对象类的引用。Arrays有一个额外的字用于数组的长度。每个对象被分配了8bytes大小的边界用于性能。对象的fields被重排序,把声明顺序变为依据byte大小的顺序

  1. doubles (8) and longs (8)
  2. ints (4) and floats (4)
  3. shorts (2) and chars (2)
  4. booleans (1) and bytes (1)
  5. references (4/8)
可以把7个long的field放入cache line,在Disruptor中,RingBuffer的cursor和BatchEventProcessor的sequences被放入cache line中。为了展示性能,这里使用一些线程来独立更新他们的计数器。计数器是volatile long类型。

代码如下

public final class FalseSharing
    implements Runnable
{
    public final static int NUM_THREADS = 4; // change
    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 FalseSharing(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 FalseSharing(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;
        public long p1, p2, p3, p4, p5, p6; // comment out
    }
}

结果如下:

运行上述代码,通过增加和删除cache line,改变线程个数,得到如下图的结果。

Java 伪共享(False Sharing)_第2张图片

伪共享的影响一目了然。在没有cache line竞争的情况下,可以达到线性。



你可能感兴趣的