《学习随笔-1》——缓存行与伪共享

缓存行

        由于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);

    }
}

运行结果如下:

在这里插入图片描述

跳跃存取:

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);

    }
}

运行结果如下:

在这里插入图片描述

在进行了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

更详细介绍参考: https://www.jianshu.com/p/7f89650367b8

发布了2 篇原创文章 · 获赞 0 · 访问量 32

猜你喜欢

转载自blog.csdn.net/qq_35217741/article/details/105309205