第一次接触伪共享的概念,是在马丁的博客上;而ifeve也把这一系列博文翻译整理好了。概读了几回,感受到此概念的重要。所以有了这个系列的第二篇读后总结。html
在上一篇博文知道,缓存的存储方式,是以缓存行(Cache Line)为单位的。通常缓存行的大小是64字节。这意味着,小于64字节的变量,是有可能存在于同一条缓存行的。例如变量X大小32字节,变量Y大小32字节,那么他们有可能会存在于一条缓存行上。java
根据马丁博客上的定义,伪共享,就是多个线程同时修改共享在同一个缓存行里的独立变量,无心中影响了性能。git
借助马丁的图,咱们能够窥知伪共享发生的过程。github
当核心1上的线程想更新X,而核心2上的线程想更新Y,而X变量和Y变量在同一个缓存行中时;每一个线程都要去竞争缓存行的全部权来更新变量。若是核心1得到所缓存行的全部权,那么缓存子系统将会使核心2中对应的缓存行失效,反之亦然。这会来来回回的通过L3缓存,大大影响了性能。这种状况,就像多个线程同事竞争锁的全部权同样。若是互相竞争的核心位于不一样的插槽,就要额外横跨插槽链接,问题可能更加严重。缓存
很遗憾,没有特别直接有效的方法。马丁本身也认可,伪共享至关难发现,所以有“无声性能杀手”之称。但这不意味着没法发现。经过观察L2和L3的缓存命中和丢失的状况,能够从侧面发现是否有伪共享的发生。多线程
对于伪共享这种影响性能的问题,解决是关键。解决伪共享的方法是经过补齐(Padding),使得每一条缓存行只存一个多线程变量。请看下面的代码:工具
public final class FalseSharing implements Runnable { public final static int NUM_THREADS = 2; // 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 { //test the size of VolatileLong System.out.println(ClassLayout.parseClass(VolatileLong.class).toPrintable()); 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 in order to trigger false sharing } }
改变线程的数量以及运行此程序(基于Intel Xeon E31270 8GB/64bit Win7/64bit jdk 6),会获得如下的结果:布局
这个结果没有马丁的结果那么惊人,伪共享在3-4线程的时候会比较明显地影响性能。这个结果后面还会继续分析。然而这并非个完美的测试,由于咱们不能肯定这些VolatileLong会布局在内存的什么位置。它们是独立的对象。可是经验告诉咱们同一时间分配的对象趋向集中于一块。性能
上面是马丁对伪共享的初步解释。说实话,解释得略微简略了一点。读了几回,仍是有不太明白的地方,所以厚着脸皮在这里发了个帖子问点疑惑,结果获得马丁本人,Nitsan和Peter等众大神的回答,收益匪浅。下面摘录点个人疑惑和他们的解答,更好的理解伪共享:测试
要回答这个问题,首先得稍微了解CPU缓存工做的协议,MESI。这套协议是用来保证CPU缓存的一致性的(cache coherency)。简单来讲,这协议定义了多级缓存下的同一个变量改变后,该怎么办。这套协议至关复杂,这里只是介绍伪共享相关的知识点,来回答咱们的问题。
咱们知道,缓存的最小使用单位,是缓存行。如上面所假设,变量X和变量Y不幸在同一个缓存行里,而核心1须要X,核心2须要Y。这时候,核心1就会拷贝这条缓存行到本身的L1,核心2也同样。因此这条缓存行在L3,核心1的L1和核心2的L1里,正如上图所示。
假设核心1修改变量X,那么根据MESI协议,这个缓存行的状态就会变成M(Modified),表面这一行数据和内存数据不一致,得回写(write back)此缓存行到L3里。而这时,须要发送一个Request For Ownership (RFO),来得到L3的这条缓存行的全部权。因为X和Y在同一条缓存行,虽然核心2修改的变量是Y,但也须要作一样的事情-发送RFO得到L3同一条缓存行的全部权。所以,伪共享就这样在L3里发生了。
会的。Java 7淘汰或是从新排列了无用的字段,所以上述的补齐在Java 7里已经失效了,伪共享还会发生。要避免伪共享,须要改变补齐的方式以下:
public static long sumPaddingToPreventOptimisation(final int index) { PaddedAtomicLong v = longs[index]; return v.p1 + v.p2 + v.p3 + v.p4 + v.p5 + v.p6; } public static class PaddedAtomicLong extends AtomicLong { public volatile long p1, p2, p3, p4, p5, p6 = 7L; }
这个方法的由来在这里,并不打算深究。要注意的是,这个方法sumPaddingToPreventOptimisation只是用来防止JVM7消除无用的字段。
理论上,咱们知道在64bit Hotspot JVM下,一个long占8字节之类的知识。但实际的对象占多少字节,怎么分布,得依靠这个工具--JOL来测量。下面是补齐前和补齐后,VolatileLong的输出:
VolatileLong object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (alignment/padding gap) N/A
16 8 long VolatileLong.value N/A
Instance size: 24 bytes (estimated, the sample instance is not available)
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
VolatileLong object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (alignment/padding gap) N/A
16 8 long VolatileLong.value N/A
24 8 long VolatileLong.p1 N/A
32 8 long VolatileLong.p2 N/A
40 8 long VolatileLong.p3 N/A
48 8 long VolatileLong.p4 N/A
56 8 long VolatileLong.p5 N/A
64 8 long VolatileLong.p6 N/A
Instance size: 72 bytes (estimated, the sample instance is not available)
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
这样,咱们能够看到补齐前,VolatileLong只有24字节小于缓存行大小,补齐后就超过缓存行大小。
假设补齐后,VolatileLong是72字节,紧接着恰好有一个变量Z是恰好56个字节,那么第二个缓存行存放着VolatileLong的8字节那一部分,以及变量Z。那么同时访问VolatileLong和Z,会不会发生伪共享呢?是否是必定要补齐到缓存行大小才彻底避免伪共享呢?
答案是否认的,补齐超过缓存行,最多浪费点珍贵的缓存,但不会产生伪共享。请看下面的图:
| 8b | 16 | 24 | 32 | 40 | 48 | 56 | 64 |
本文完