时间局部性:CPU读取数据时的顺序为寄存器->L1->L2->L3->内存,当从内存中读取到数据时,会一次将数据放入L3、L2、L1中,这样下次CPU再读取这个数据时直接从L1中获取,无需读取内存。CPU认为程序在短时间内有多次操作同一个数据的倾向,所以会将数据存储在高速缓存中。
空间局部性:CPU认为从内存中读取一个数据,下一次访问的很有可能是它旁边的数据,所以会进行预读取,目前工业界一次性预读取的大小一般为64Byte,这64个字节的大小一般称为缓冲行,也就是说CPU在读取数据的时候一次性读取一个缓冲行大小。
下面通过一个例子来证明缓冲行的存在:
例1:
package com.morris.concurrent.volatiledemo;
public class CacheLinePadding {
private static class T {
public volatile long x = 0L;
}
public static T[] arr = new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
for (long i = 0; i < 1000_0000L; i++) {
arr[0].x = i;
}
});
Thread t2 = new Thread(() -> {
for (long i = 0; i < 1000_0000L; i++) {
arr[1].x = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime() - start) / 1_000_000);
}
}
例2:将例1中的T类换成下面的,其余保持不变:
private static class T {
public volatile long p1, p2, p3, p4, p5, p6, p7;
public volatile long x = 0L;
public volatile long p8, p9, p10, p11, p12, p13, p14;
}
运行结果如下:
例1:2710ms
例2:1270ms
运行结果原因分析:
例1:arr[0].x与arr[1].x很大可能位于同一个缓存行中,这样多个核内的高速缓存中都有arr[0].x和arr[1].x的副本,当线程t1对arr[0].x进行修改时会使得其他核内的缓存行失效,导致其他线程每次对数据的操作需要从先从内存中读取,没有充分利用高速缓存,影响性能。
例2:变量x的前后被填充了7个long型变量,也就是前后填充了56个字节,加上x自身总共64个字节,而缓存行的大小为64个字节,这样就保证了arr[0].x与arr[1].x一定不会在同一个缓存行,当线程t1和线程t2分别对这样arr[0].x与arr[1].x进行写操作时,无需使用缓存一致性协议来保证数据的一致性,充分利用了高速缓存,所以性能有所提升。
jdk提供了@sun.misc.Contended注解来实现缓存行对齐,无需手动填充变量了,在运行时需要设置JVM启动参数-XX:-RestrictContended,使用方法如下:
private static class T {
@sun.misc.Contended
public volatile long x = 0L;
}