在多线程的程序中,我们大部分都是从应用的级别关注共享资源的同步问题,对于底层的资源共享或者冲突问题很少关注,其实伪共享的问题在多线程中是一个效率的隐形杀手,因为很多时候我们从代码中并看不到这个问题的存在。
什么时候产生伪共享
CPU的缓存系统都是以缓存行(cache line)为单位存储的, 缓存行是2的整数幂个连续字节(一般为32~256个字节),最常见的缓存行大小是64个字节。当多线程程序同时修改的两个变量存在在一个缓存行中时,就会引起缓存行级别的互斥,这就是伪共享。
在CPU不同的核中缓存行的同步是通过MESI协议进行同步的,该协议可以保证缓存数据的一致性,当不同核的cache数据不一致的时候就会引起数据的同步问题,同步主要是通过内存进行的,因此对效率的影响是比较大的。
举例说明:
上图说明了伪共享的问题。在核1上运行的线程想更新变量X,同时心2上的线程想要更新变量Y,但是这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果核1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效。当核2获得了所有权然后执行更新操作,核1就要使自己对应的缓存行失效。没事失效后,如果再次使用数据就要从L3中重新获取数据,这样就会来回的经过L3缓存,大大影响了性能。如果互相竞争的核位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。
避免伪共享
我们可以通过将不同的数据放在不同的缓存行就可以避免伪共享的产生,我们看下在java中是怎么解决的 在JDK6中对于变量的缓存主要是通过增加变量来空间填充进行的,例如:
//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都存放在不同的缓存行中。
在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
}
}
复制代码
运行上面的代码,增加线程数以及添加/移除缓存行的填充性能对比如下:
从上图能够明显看出伪共享的影响,没有缓存行竞争时,我们几近达到了随着线程数的线性扩展。
上面的文章很多是翻译的Martin Thompson的文章,地址如下: https://mechanical-sympathy.blogspot.com/2011/07/false-sharing.html