缓存行
由于CPU的速度远远大于内存速度,为提高CPU的速度,CPU中加入了缓存(cache),缓存分为三级L1,L2,L3。级别越小越接近CPU, 速度更快, 同时容量越小。每个缓存里面是以缓存行为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节,最常见的缓存行大小是64个字节。
CPU 访问内存时,首先查询 cache 是否已缓存该数据。如果有,则返回数据,无需访问内存;如果不存在,则需把数据从内存中载入 cache,最后返回给CPU。若cache命中率高,这会极大提高性能。
缓存行命中测试代码
一个long类型占8个字节,8个long类型共64字节可以填充一个缓存行,为使对比明显,建立一个8 * 1000000的long数组,一次顺序存取,一次跳跃存取,对比运行时间:
顺序存取:
public final class CacheLineTest {
//填充10000000个缓存行,每行8个long,共64字节
private static final long[] values = new long[8 * 10000000];
public static void main(String[] args) throws Exception {
long time = System.nanoTime();
for (int i = 0; i < 10000000; i++) {
//顺序存取
values[i] = i;
}
time = System.nanoTime() - time;
System.out.println("顺续存取耗时:" + time);
}
}
运行结果如下:
![](https://img.haomeiwen.com/i23728960/41238aa2c3048f3a.png)
跳跃存取:
public final class CacheLineTest {
//填充10000000个缓存行,每行8个long,共64字节
private static final long[] values = new long[8 * 10000000];
public static void main(String[] args) throws Exception {
long time = System.nanoTime();
for (int i = 0; i < 10000000; i++) {
//跳跃存取
values[i*8] = i;
}
time = System.nanoTime() - time;
System.out.println("跳跃存取耗时:" + time);
}
}
运行结果如下:
![](https://img.haomeiwen.com/i23728960/c96e6287440aadb6.png)
在进行了1000万次存取下,差距还是比较明显的。
伪共享
假设有两个变量X,Y,多线程访问中,运行于CPU1的线程访问X,运行于CPU2的线程访问Y,尽管两个变量之间没有任何关系,但是在线程之间仍然需要同步。比如CPU1的线程改变了X,同一行的Y及时没有更新也会失效,导致CPU2访问Y时缓存无法命中,同理,CPU2对Y的更新也会影响到X,反反复复,影响性能。
为了避免这种情况,一种可行的做法是填充缓存行,使一个缓存行中,只有一个变量实际是有效的。
伪共享测试代码
声明一个对象demo,对象头占8字节,一个long类型占8字节,共16个字节,连续创建4个实例,多半在同一个缓存行中,起四个线程分别对四个实例的value做大量的存取操作查看其运行时间。然后在demo填充6个8字节的long,填满64字节,重新运行查看其运行时间。
public final class FlashShareDemo {
//测试对象,对象头占8字节,声明一个long类型8字节,
//共16字节,连续声明4个demo对象,多半在同一个缓存行中
static class Demo{
//需要操作的对象声明为volatile以便其他线程可以看到其变化
public volatile long value = 0L;
//填充long,每个8字节,填充6个,共64字节,刚好一个缓存行
//private long P1,P2,P3,P4,P5,P6;
}
//启动线程对demo中的value值做大量存取操作
static final class TestThread extends Thread{
private Demo demo;
public TestThread(final Demo demo){
this.demo = demo;
}
@Override
public void run(){
long start = System.currentTimeMillis();
for(int i = 0 ; i < 100000000; i++){
demo.value = i;
}
start = System.currentTimeMillis() - start;
System.out.println(Thread.currentThread().getName() + "运行耗时" + start);
}
}
public static void main(String[] args) throws Exception{
Demo[] Demo = new Demo[4];
//启动四个线程进行测试
for(int i = 0; i < 4; i++){
Demo[i] = new Demo();
}
TestThread[] testThread = new TestThread[Demo.length];
for (int i = 0; i < Demo.length ; i++ ){
testThread[i] = new TestThread(Demo[i]);
}
long start = System.currentTimeMillis();
for(Thread t : testThread){
t.start();
}
for(Thread t : testThread){
t.join();
}
start = System.currentTimeMillis() - start;
System.out.println("未填充对象访问耗时:" + start);
}
}
未填充运行结果如下:
由此可以看出,第一个启动线程运行最快,后续三个线程运行时间大致相当,均远长于第一个启动线程。
填充完后(放开demo中注释掉的填充代码)运行结果如下:
四个线程互不干扰,运行时间大致相当,均与未填充时第一个启动线程相当。
附:
在Java8中提供了@sun.misc.Contended来避免伪共享,在运行时需要设置JVM启动参数-XX:-RestrictContended
网友评论