伪共享和缓存行填充,从Java 6, Java 7 到Java 8

关于伪共享的文章已经很多了,对于多线程编程来说,特别是多线程处理列表和数组的时候,要非常注意伪共享的问题。否则不仅无法发挥多线程的优势,还可能比单线程性能还差。随着JAVA版本的更新,再各个版本上减少伪共享的做法都有区别,一不小心代码可能就失效了,要注意进行测试。这篇文章总结一下。

什么是伪共享

关于伪共享讲解最清楚的是这篇文章《剖析Disruptor:为什么会这么快?(三)伪共享》,我这里就直接摘抄其对伪共享的解释:

进群:697699179可以获取Java各类入门学习资料!

这是我的微信公众号【编程study】各位大佬有空可以关注下,每天更新Java学习方法,感谢!

学习中遇到问题有不明白的地方,推荐加小编Java学习群:697699179内有视频教程 ,直播课程 ,等学习资料,期待你的加入

缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如 果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素。有 人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。

为了让可伸缩性与线程数呈线性关系,就必须确保不会有两个线程往同一个变量或缓存行中写。两个线程写同一个变量可以在代码中发现。为了确定互相独立的变量 是否共享了同一个缓存行,就需要了解内存布局,或找个工具告诉我们。Intel VTune就是这样一个分析工具。本文中我将解释Java对象的内存布局以及我们该如何填充缓存行以避免伪共享。

伪共享和缓存行填充,从Java 6, Java 7 到Java 8_第1张图片

图1说明了伪共享的问题。在核心1上运行的线程想更新变量X,同时核心2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去 竞争缓存行的所有权来更新变量。如果核心1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效。当核心2获得了所有权然后执行更新操作,核心1就要 使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。


JAVA 6下的方案

解决伪共享的办法是使用缓存行填充,使一个对象占用的内存大小刚好为64bytes或它的整数倍,这样就保证了一个缓存行里不会有多个对象。《剖析Disruptor:为什么会这么快?(三)伪共享》提供了缓存行填充的例子:

publicfinalclass FalseSharing

    implements Runnable

{

    publicfinalstaticintNUM_THREADS = 4;// change publicfinalstaticlongITERATIONS = 500L * 1000L * 1000L;

    privatefinalint arrayIndex;


    privatestaticVolatileLong[] longs =new VolatileLong[NUM_THREADS];

    static

    {

        for(inti = 0; i < longs.length; i++)

        {

            longs[i] =new VolatileLong();

        }

    }


    publicFalseSharing(finalint arrayIndex)

    {

        this.arrayIndex = arrayIndex;

    }


    publicstaticvoidmain(finalString[] args)throws Exception

    {

        finallongstart = System.nanoTime();

        runTest();

        System.out.println("duration = " + (System.nanoTime() - start));

    }


    privatestaticvoidrunTest()throws InterruptedException

    {

        Thread[] threads =new Thread[NUM_THREADS];


        for(inti = 0; i < threads.length; i++)

        {

            threads[i] =newThread(new FalseSharing(i));

        }


        for (Thread t : threads)

        {

            t.start();

        }


        for (Thread t : threads)

        {

            t.join();

        }

    }


    publicvoid run()

    {

        longi = ITERATIONS + 1;

        while(0 != --i)

        {

            longs[arrayIndex].value = i;

        }

    }


    publicfinalstaticclass VolatileLong

    {

        publicvolatilelongvalue = 0L;

        publiclongp1, p2, p3, p4, p5, p6;// comment out    }

}


VolatileLong通过填充一些无用的字段p1,p2,p3,p4,p5,p6,再考虑到对象头也占用8bit, 刚好把对象占用的内存扩展到刚好占64bytes(或者64bytes的整数倍)。这样就避免了一个缓存行中加载多个对象。但这个方法现在只能适应JAVA6 及以前的版本了。


(注:如果我们的填充使对象size大于64bytes,比如多填充16bytes– public long p1, p2, p3, p4, p5, p6, p7, p8;。理论上同样应该避免伪共享问题,但事实是这样的话执行速度同样慢几倍,只比没有使用填充好一些而已。还没有理解其原因。所以测试下来,必须是64bytes的整数倍)


JAVA 7下的方案

上面这个例子在JAVA 7下已经不适用了。因为JAVA 7会优化掉无用的字段,可以参考《False Sharing && Java 7》。


因此,JAVA 7下做缓存行填充更麻烦了,需要使用继承的办法来避免填充被优化掉,《False Sharing && Java 7》里的例子我觉得不是很好,于是我自己做了一些优化,使其更通用:

publicfinalclassFalseSharingimplements Runnable { 

    publicstaticintNUM_THREADS = 4;// change  publicfinalstaticlongITERATIONS = 500L * 1000L * 1000L; 

    privatefinalint arrayIndex; 

    privatestatic VolatileLong[] longs; 


    publicFalseSharing(finalint arrayIndex) { 

        this.arrayIndex = arrayIndex; 

    } 


    publicstaticvoidmain(finalString[] args)throws Exception { 

        Thread.sleep(10000); 

        System.out.println("starting...."); 

        if(args.length == 1) { 

            NUM_THREADS = Integer.parseInt(args[0]); 

        } 


        longs =new VolatileLong[NUM_THREADS]; 

        for(inti = 0; i < longs.length; i++) { 

            longs[i] =new VolatileLong(); 

        } 

        finallongstart = System.nanoTime(); 

        runTest(); 

        System.out.println("duration = " + (System.nanoTime() - start)); 

    } 


    privatestaticvoidrunTest()throws InterruptedException { 

        Thread[] threads =new Thread[NUM_THREADS]; 

        for(inti = 0; i < threads.length; i++) { 

            threads[i] =newThread(new FalseSharing(i)); 

        } 

        for (Thread t : threads) { 

            t.start(); 

        } 

        for (Thread t : threads) { 

            t.join(); 

        } 

    } 


    publicvoid run() { 

        longi = ITERATIONS + 1; 

        while(0 != --i) { 

            longs[arrayIndex].value = i; 

        } 

    } 

}

publicclass VolatileLongPadding {

    publicvolatilelongp1, p2, p3, p4, p5, p6;// 注释  }

publicclassVolatileLongextends VolatileLongPadding {

    publicvolatilelongvalue = 0L; 

}


把padding放在基类里面,可以避免优化。(这好像没有什么道理好讲的,JAVA7的内存优化算法问题,能绕则绕)。不过,这种办法怎么看都有点烦,借用另外一个博主的话:做个java程序员真难。



JAVA 8下的方案

在JAVA 8中,缓存行填充终于被JAVA原生支持了。JAVA 8中添加了一个@Contended的注解,添加这个的注解,将会在自动进行缓存行填充。以上的例子可以改为:

publicfinalclassFalseSharingimplements Runnable { 

    publicstaticintNUM_THREADS = 4;// change  publicfinalstaticlongITERATIONS = 500L * 1000L * 1000L; 

    privatefinalint arrayIndex; 

    privatestatic VolatileLong[] longs; 


    publicFalseSharing(finalint arrayIndex) { 

        this.arrayIndex = arrayIndex; 

    } 


    publicstaticvoidmain(finalString[] args)throws Exception { 

        Thread.sleep(10000); 

        System.out.println("starting...."); 

        if(args.length == 1) { 

            NUM_THREADS = Integer.parseInt(args[0]); 

        } 


        longs =new VolatileLong[NUM_THREADS]; 

        for(inti = 0; i < longs.length; i++) { 

            longs[i] =new VolatileLong(); 

        } 

        finallongstart = System.nanoTime(); 

        runTest(); 

        System.out.println("duration = " + (System.nanoTime() - start)); 

    } 


    privatestaticvoidrunTest()throws InterruptedException { 

        Thread[] threads =new Thread[NUM_THREADS]; 

        for(inti = 0; i < threads.length; i++) { 

            threads[i] =newThread(new FalseSharing(i)); 

        } 

        for (Thread t : threads) { 

            t.start(); 

        } 

        for (Thread t : threads) { 

            t.join(); 

        } 

    } 


    publicvoid run() { 

        longi = ITERATIONS + 1; 

        while(0 != --i) { 

            longs[arrayIndex].value = i; 

        } 

    } 

}

importsun.misc.Contended;@Contendedpublicclass VolatileLong {

    publicvolatilelongvalue = 0L; 

}


执行时,必须加上虚拟机参数-XX:-RestrictContended,@Contended注释才会生效。很多文章把这个漏掉了,那样的话实际上就没有起作用。


@Contended注释还可以添加在字段上,今后再写文章详细介绍它的用法。


(后记:以上代码基于32位JDK测试,64位JDK下,对象头大小不同,有空再测试一下)

你可能感兴趣的:(伪共享和缓存行填充,从Java 6, Java 7 到Java 8)