在介绍缓存行之前,我们需要先了解操作系统的存储器的层次结构,下图为CSAPP(原书第三版)中存储器层次结构图:
存储器层次结构图
一般的程序执行都是由CPU执行,但是由于CPU与主存的速度差异过大,因此引入了CPU缓存,一般分为三级缓存(至于为什么是三级,不是二级,四级,是因为工业系统长期实际得出的结果,三级缓存为最优):L1,L2,L3,与CPU更接近则访问速度越快。CPU访问缓存的顺序是L1,L2,L3,L1没命中,则去访问L2;L2没命中,则去访问L3,以此类推,一直到命中为止。而在L1,L2,L3中存储的数据并不是以某个变量单独存储的,在每个缓存里面是以缓存行(cache line)为单位存储的(这也是CPU的实现规定的),一般来说一个缓存行的大小在32kb-256kb之间,而通常的缓存行大小则为64kb(同样为工业得出的最优解)。这里带出来一个隐藏的问题就是:多线程修改互相独立的变量且这些变量位于同一个缓存行时,这会影响性能且很难发现,这也被称为伪共享。看以下模拟伪共享代码(即两个变量处于同一个缓存行):
public class CacheLineFalse {
private static long operationTimes=100000000L;
private static class T{
public volatile long data=0L;
}
public static T[] array= new T[2];
static {
//分配空间
array[0]=new T();
array[1]=new T();
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch=new CountDownLatch(2);
// 线程1操作array[0].x
Thread thread1=new Thread(()->{
for (int i=0;i<operationTimes;i++){
array[0].data=i;
}
latch.countDown();
});
//线程2操作array[1].x
Thread thread2=new Thread(()->{
for (long i = 0; i < operationTimes; i++) {
array[1].data=i;
}
latch.countDown();
});
final long startTime=System.nanoTime();
thread1.start();
thread2.start();
latch.await();
System.out.println((System.nanoTime()-startTime)/100_0000);
}
}
执行结果如下:
伪共享执行结果
再来看两个变量处于不同的缓存行:
public class CacheLineTrue {
private static long operationTimes=100000000L;
private static class T{
//一个long占8个字节,8个long类型的刚好满一个缓存行
private long p1,p2,p3,p4,p5,p6,p7;
public volatile long data=0L;
private long p9,p10,p11,p12,p13,p14,p15;
}
public static CacheLineTrue.T[] array= new CacheLineTrue.T[2];
static {
//分配空间
array[0]=new CacheLineTrue.T();
array[1]=new CacheLineTrue.T();
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch=new CountDownLatch(2);
// 线程1操作array[0].x
Thread thread1=new Thread(()->{
for (int i=0;i<operationTimes;i++){
array[0].data=i;
}
latch.countDown();
});
//线程2操作array[1].x
Thread thread2=new Thread(()->{
for (long i = 0; i < operationTimes; i++) {
array[1].data=i;
}
latch.countDown();
});
final long startTime=System.nanoTime();
thread1.start();
thread2.start();
latch.await();
System.out.println((System.nanoTime()-startTime)/1000_000);
}
}
执行结果如下:
处于不同缓存行执行结果
可以看到在时间上还是有差距的,那么来看下两个代码的区别:
区别
为什么多定义了几个long变量,就导致这么大的差距?下面解释下原因:
一个long变量占8个字节,而一个缓存行占64字节,8个long类型的变量刚好充满一个缓存行,而左边这样定义的结果就会导致两个线程访问的data一定不在同一个缓存行内,而右边这样定义会导致两个线程需要重新读取缓存行的内容(假设线程1获得了CPU执行权,volatile的作用会让线程2读取的缓存行失效。当线程2获得了CPU执行权然后执行更新操作,线程1读取的缓存行则会失效),把时间都耗费在这上面,由此产生的时间差,以及带来的性能问题就是这样产生的。
Java8为了避免这种伪共享,实现了字节填充。
JVM参数 -XX:-RestrictContended
@Contended 位于 sun.misc 用于注解java 属性字段,自动填充字节,防止伪共享
网友评论