关于伪共享的文章已经不少了,对于多线程编程来讲,特别是多线程处理列表和数组的时候,要很是注意伪共享的问题。不然不只没法发挥多线程的优点,还可能比单线程性能还差。随着JAVA版本的更新,再各个版本上减小伪共享的作法都有区别,一不当心代码可能就失效了,要注意进行测试。这篇文章总结一下。html
关于伪共享讲解最清楚的是这篇文章《剖析Disruptor:为何会这么快?(三)伪共享》,我这里就直接摘抄其对伪共享的解释:java
缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,通常为32-256个字节。最多见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如 果这些变量共享同一个缓存行,就会无心中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素。有 人将伪共享描述成无声的性能杀手,由于从代码中很难看清楚是否会出现伪共享。程序员
为了让可伸缩性与线程数呈线性关系,就必须确保不会有两个线程往同一个变量或缓存行中写。两个线程写同一个变量能够在代码中发现。为了肯定互相独立的变量 是否共享了同一个缓存行,就须要了解内存布局,或找个工具告诉咱们。Intel VTune就是这样一个分析工具。本文中我将解释Java对象的内存布局以及咱们该如何填充缓存行以免伪共享。算法
图1说明了伪共享的问题。在核心1上运行的线程想更新变量X,同时核心2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每一个线程都要去 竞争缓存行的全部权来更新变量。若是核心1得到了全部权,缓存子系统将会使核心2中对应的缓存行失效。当核心2得到了全部权而后执行更新操做,核心1就要 使本身对应的缓存行失效。这会来来回回的通过L3缓存,大大影响了性能。若是互相竞争的核心位于不一样的插槽,就要额外横跨插槽链接,问题可能更加严重。编程
解决伪共享的办法是使用缓存行填充,使一个对象占用的内存大小恰好为64bytes或它的整数倍,这样就保证了一个缓存行里不会有多个对象。《剖析Disruptor:为何会这么快?(三)伪共享》提供了缓存行填充的例子:数组
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 } }
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会优化掉无用的字段,能够参考《False Sharing && Java 7》。工具
所以,JAVA 7下作缓存行填充更麻烦了,须要使用继承的办法来避免填充被优化掉,《False Sharing && Java 7》里的例子我以为不是很好,因而我本身作了一些优化,使其更通用:布局
public final class FalseSharing implements Runnable { public static int NUM_THREADS = 4; // change public final static long ITERATIONS = 500L * 1000L * 1000L; private final int arrayIndex; private static VolatileLong[] longs; public FalseSharing(final int arrayIndex) { this.arrayIndex = arrayIndex; } public static void main(final String[] 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 (int i = 0; i < longs.length; i++) { longs[i] = new VolatileLong(); } 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 class VolatileLongPadding { public volatile long p1, p2, p3, p4, p5, p6; // 注释 }
public class VolatileLong extends VolatileLongPadding { public volatile long value = 0L; }
把padding放在基类里面,能够避免优化。(这好像没有什么道理好讲的,JAVA7的内存优化算法问题,能绕则绕)。不过,这种办法怎么看都有点烦,借用另一个博主的话:作个java程序员真难。
在JAVA 8中,缓存行填充终于被JAVA原生支持了。JAVA 8中添加了一个@Contended的注解,添加这个的注解,将会在自动进行缓存行填充。以上的例子能够改成:
public final class FalseSharing implements Runnable { public static int NUM_THREADS = 4; // change public final static long ITERATIONS = 500L * 1000L * 1000L; private final int arrayIndex; private static VolatileLong[] longs; public FalseSharing(final int arrayIndex) { this.arrayIndex = arrayIndex; } public static void main(final String[] 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 (int i = 0; i < longs.length; i++) { longs[i] = new VolatileLong(); } 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; } } }
import sun.misc.Contended; @Contended public class VolatileLong { public volatile long value = 0L; }
执行时,必须加上虚拟机参数-XX:-RestrictContended,@Contended注释才会生效。不少文章把这个漏掉了,那样的话实际上就没有起做用。
@Contended注释还能够添加在字段上,从此再写文章详细介绍它的用法。
(后记:以上代码基于32位JDK测试,64位JDK下,对象头大小不一样,有空再测试一下)
参考
http://mechanical-sympathy.blogspot.com/2011/07/false-sharing.html
http://mechanical-sympathy.blogspot.hk/2011/08/false-sharing-java-7.html
http://robsjava.blogspot.com/2014/03/what-is-false-sharing.html