CPU高速缓存架构
1.每次读取数据都会从L1开始读取,L1找不到再去L2,L2找不到再去L3,L3找不到就去主内存读 2. 2.容量:L3>L2>L1 速度:L1>L2>L3
3.每次读取最小单位缓存行:64字节,可以理解为缓存中的数据是一行一行的,一次读写一行
4.保持缓存一致性:MESI协议
Volatile作用
1.防止指令重排
2.每次对变量的修改,都会将CPU核高速缓存写回到主内存
3.一个CPU核高速缓存写回到主内存会导致其它CPU核的高速缓存无效,其它CPU核读取的时候会从主内存重新读(MESI协议)
Volatile测试
1.定义一个类
public static class VolatileBean {
volatile long a;
volatile long b;
}
2.开启两条线程各自对a和b执行操作
VolatileBean volatileBean = new VolatileBean();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10_000_000; i++) {
volatileBean.a = i;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10_000_000; i++) {
volatileBean.b = i;
}
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(System.currentTimeMillis()-start);
多次执行输出:大概在40到60之间,也有可能会输出17
3.对VolatileBean进行修改,多增加几个字段
public static class VolatileBean {
volatile long a;
long p1;
long p2;
long p3;
long p4;
long p5;
long p6;
long p7;
volatile long b;
}
然后再执行上面步骤 2的代码 多次执行输出:大概在14到18之间
分析
为什么VolatileBean添加了几个字段之后速度提升了?
-
上面提到过CPU缓存行是64字节,而我们的代码中long类型是8字节,一开始只有a、b两个字段的时候,一共占了16字节,在同一个缓存行中
-
添加了p1到p7一共7个long类型后,a到p7一共8个long类型,占了64字节,填满缓存行,所以b字段只能放到另外的缓存行,保证了a和b不在同一个缓存行
-
上面说过了,volatile修饰的字段,修改的时候会写回到主内存并导致其它CPU核的高速缓存中对于字段的值的缓存行无效(CPU读写最小单位64字节),所以
- a和b在同一个缓存行,a修改会导致另外一个CPU核中a所在的缓存行无效,即b的值也无效,需要重新读取主内存
- a和b不在同一个缓存行,a或b的改动不会影响到另外一个值所在的缓存行无效,即下次另外一个线程读取的时候直接从高速缓存中读取就行
思考
上面说过,当只有a、b两个线程的时候,也有几率会出现花费时间为17,我认为有可能是两个线程刚好运行在同一个CPU核上,还没去验证