在多线程的程序中,咱们大部分都是从应用的级别关注共享资源的同步问题,对于底层的资源共享或者冲突问题不多关注,其实伪共享的问题在多线程中是一个效率的隐形杀手,由于不少时候咱们从代码中并看不到这个问题的存在。html
CPU的缓存系统都是以缓存行(cache line)为单位存储的, 缓存行是2的整数幂个连续字节(通常为32~256个字节),最多见的缓存行大小是64个字节。当多线程程序同时修改的两个变量存在在一个缓存行中时,就会引发缓存行级别的互斥,这就是伪共享。java
在CPU不一样的核中缓存行的同步是经过MESI协议进行同步的,该协议能够保证缓存数据的一致性,当不一样核的cache数据不一致的时候就会引发数据的同步问题,同步主要是经过内存进行的,所以对效率的影响是比较大的。缓存
举例说明:多线程
上图说明了伪共享的问题。在核1上运行的线程想更新变量X,同时心2上的线程想要更新变量Y,可是这两个变量在同一个缓存行中。每一个线程都要去竞争缓存行的全部权来更新变量。若是核1得到了全部权,缓存子系统将会使核心2中对应的缓存行失效。当核2得到了全部权而后执行更新操做,核1就要使本身对应的缓存行失效。没事失效后,若是再次使用数据就要从L3中从新获取数据,这样就会来回的通过L3缓存,大大影响了性能。若是互相竞争的核位于不一样的插槽,就要额外横跨插槽链接,问题可能更加严重。性能
咱们能够经过将不一样的数据放在不一样的缓存行就能够避免伪共享的产生,咱们看下在java中是怎么解决的 在JDK6中对于变量的缓存主要是经过增长变量来空间填充进行的,例如:this
//ConcurrentHashMapV8.java的实现
public final static class VolatileLong {
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; // comment out
}
复制代码
对于HotSpot JVM中,在每一个对象中还有连个字的对象头(4字节*2),所以上面的CounterCell就能够看出分别是放到了两个缓存行中,只索引是两个缓存行主要是为了适应64位的JVM,这样能保证任何的两个CounterCell都存放在不一样的缓存行中。spa
在JDK8中主要经过注解的方式解决:线程
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
...
}
复制代码
经过下面的例子真实的模拟下性能的差别翻译
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
}
}
复制代码
运行上面的代码,增长线程数以及添加/移除缓存行的填充性能对好比下:code
从上图可以明显看出伪共享的影响,没有缓存行竞争时,咱们几近达到了随着线程数的线性扩展。
上面的文章不少是翻译的Martin Thompson的文章,地址以下: https://mechanical-sympathy.blogspot.com/2011/07/false-sharing.html