什么是缓存行?
在cpu进行读取数据的时候,会向内存中拿数据,而cpu和内存读取数据的速度差别很大,所有加入了缓存进行了缓冲,大大提高了cpu从内存拿去数据的速度。缓存又分一级缓存、二级缓存、三级缓存,读取数据的速度依次降低,而这些缓存存储数据的基本单位就是缓存行。
通常情况下,存储器价格会随着速度的上升而上升,因为高速存储器的成本十分高昂。比如寄存器最快,价格也最贵;内存慢许多,价格也便宜很多。而寄存器与内存的速度差异十分巨大,因此CPU就引入了速度和价格都在二者中间的cpu缓存来平衡这种差异。
当CPU尝试访问某个变量时,会先在一级中查找,如果命中缓存则直接读取数据;如果没有找到,就去下一级,一直到内存,随后将该变量所在的一个缓存行大小的区域复制到缓存中。查找的路线最长,速度越慢,因此将那些访问频繁的数据应当保存在一级缓存中。另外,一个变量的大小往往小于一个缓存行的大小,这时就有可能把内存中和当前变量连续的地址一起读进缓存行(一般缓存行占64个字节)。
缓存一致性原则
多核CPU的情况下,每个核都会有一个独立的一级二级缓存。如果一个变量A被多个cpu处理器读取了数据(一个cpu几核就是几个处理器),然后存储在缓存中,当其中任意一个处理器修改了变量A,那么其他处理器存储变量A所在的缓存行失效,如果其他处理器要操作变量A,就要重新取内存中读取数据。
缓存行的优势
在单线程中,缓存行大大提高了cpu访问内存的速度,比如java遍历数据,如果按顺序遍历如以下代码只需要用时120ms。
public class Test7_psuado_share {
static final int LINE_NUM = 10240;
static final int COLUM_NUM = 10240;
public static void main(String[] args) {
long[][] array = new long[LINE_NUM][COLUM_NUM];
long startTime = System.currentTimeMillis();
//方案一:执行时间120ms
//多个地址连续的变量才有可能被放到同一个缓存行中,读取数组第一个元素时,多个元素会被放到同一个缓存行
//这样顺序访问数组元素时会在缓存行中直接命中,就不会到内存中读取了
for (int i = 0; i < LINE_NUM; i++) {
for (int j = 0; j < COLUM_NUM; j++) {
array[i][j] = i * 2 + j;
}
}
System.out.println(System.currentTimeMillis() - startTime);
}
}
而如果我们按列遍历数组,也就是地址不连续的跳动遍历 ,如以下代码,耗时2284ms。
public class Test7_psuado_share {
static final int LINE_NUM = 10240;
static final int COLUM_NUM = 10240;
public static void main(String[] args) {
long[][] array = new long[LINE_NUM][COLUM_NUM];
long startTime = System.currentTimeMillis();
// 方案二:执行时间2284ms
// 方案二是跳跃式的,破坏了程序访问的局部性原则,且缓存容量有限
// 当缓存满了时会根据一定的淘汰算法替换缓存行,从而降低程序性能
for (int i = 0; i < LINE_NUM; i++) {
for (int j = 0; j < COLUM_NUM; j++) {
array[j][i] = i * 2 + j;
}
}
System.out.println(System.currentTimeMillis() - startTime);
}
}
很明显缓存带来的效率大大提高了20倍,如上面所说,如果cpu读取long型数组第一个下标的值时, 会把之后的七个数的值全部读入一个缓存行,当继续遍历数组时,cpu直接在缓存命中,拿去数据,大大减少了取内存拿去数据的时间。
缓存行的劣势
假设我们有两个连续存放(例如数组)的int:a和b
,缓存行大小是64字节
,则ab可能被存放在一个缓存行中,我们的CPU有两个核心,恰好这两个核心分别在读写这a和b。此时会发生什么情况?
处理器1 修改了a,导致处理器2存在a的缓存行失效,而ab在一起,也就导致了b的失效,当处理器2要去操作b数据要重新取内存中读取数据,导致了缓存的加入没起到作用。
处理器2 修改了b,导致处理器1存在b的缓存行失效,而ab在一起,也就导致了a的失效,处理器1要去操作数据b要重新取内存读取数据
这种因为一个缓存行缓存多个变量,而多个核心又在修改一个缓存上的多个变量时引发的缓存失效问题,这种问题就是伪共享。
如果避免伪共享?
避免伪共享的方式往往是进行填充,例如a和b原本可以放到同一个缓存行中,在a的后面填充一些内容,使得a和b无法同时放下,这样来a和b被放到同一个缓存行中。当然,进行填充会导致缓存行大量空间的浪费,因此应当权衡场景,采取正确的策略
代码演示
代码由N个线程分别频繁访问数组的0-N-1位置的元素
VolatileLong
中只有一个被volatile修饰的long变量,多个Volatile Long可能被放在一个缓存行中。
Volatile Long2
中的变量前后都有padding变量,多个VolatileLong2不在一个缓存行中
public class Main {
private final static int CORE_COUNT = Runtime.getRuntime().availableProcessors();
private final static long LOOP = 100 * 1000 * 1000L;
// 6729ms 伪共享
// private static VolatileLong[] longs = new VolatileLong[CORE_COUNT];
// 1460ms 填充解决伪共享问题
private static VolatileLong2[] longs = new VolatileLong2[CORE_COUNT];
public static void main(final String[] args) throws Exception {
for (int i = 0; i < longs.length; i++) {
longs[i] = new VolatileLong2();
}
Thread[] threads = new Thread[Runtime.getRuntime().availableProcessors()];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new Test(i));
}
long startTime = System.currentTimeMillis();
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(System.currentTimeMillis() - startTime);
}
static class Test implements Runnable {
private int index = 0;
public Test(int index) {
this.index = index;
}
@Override
public void run() {
long i = LOOP;
while (0 != --i) {
longs[index].value = i;
}
}
}
static class VolatileLong {
public volatile long value = 0L;
}
static class VolatileLong2 {
volatile long i0, i1, i2, i3, i4, i5, i6;
public volatile long value = 0L;
volatile long j0, j1, j2, j3, j4, j5, j6;
}
}
JDK8之后,提供了一个@Contended
注解,使用它就可以进行自动填充
/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;