缓存行对齐以及MESI协议

引言​

这篇博客是来填上一篇博客写下的坑,来解释一下这个图有关的两个问题,其一是缓存行同步问题,其二是MESI协议。

CPU缓存结构

​ 先说CPU的缓存结构,在计算机组成中CPU的结构一个核心中关注这三个部分:ALU只负责计算,PC记录程序当前运行到哪一行了,Register计数器记录当前变量的值,在CPU的视野中是没有什么多线程问题的,他只在乎PC、寄存器中的值是什么拿到指令,到寄存器中计算数据然后再存回寄存器中就可以了。
缓存行对齐以及MESI协议_第1张图片
​ 那么在CPU设计的时候就想着尽可能的压榨ALU的计算性能,让他不停的计算,CPU中的寄存器运行速度是相当快的,则我们可以使用缓存的概念将未来可能要遇到的数据提前缓存在CPU中的L缓存中CPU的缓存结构设计如图所示:一颗CPU中有两个核心,诶个核心中有一级缓存,其中寄存器中有一级缓存,一颗CPU中两个核心共用一个缓存,这些缓存的速度级别越接近里面越快,速度级别差距很大。

缓存行对齐以及MESI协议_第2张图片

​ 根据计算机的局部性原理(当需要某一个数据的时候会将其相邻的数据一起调入内存中,因为其临近数据被访问的概率很高),那么在调用的时候就有缓存行的概念一个缓存行64字节,这个概念在MySQL、磁盘调页中都存在,那么由于缓存行的特性,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享,凭借肉眼是很难观察出伪共享的存在的。

缓存行存在证明

接下来通过一个例子证明缓存行的存在看下面一个小程序

package com.Xx.algorithm.test;

/**
 * @author XX
 * @date 2020/10/9 - 11:41
 */
public class V2 {
     
    private static class T{
     
        private boolean flag = true;
    }

    public static void main(String[] args) throws InterruptedException {
     
        T t = new T();
        Thread t1 = new Thread(new Runnable() {
     
            @Override
            public void run() {
     
                System.out.println("Start");
                while(t.flag){
     
                }
                System.out.println("end");
            }
        });
        t1.start();
        Thread.sleep(1000);
        t.flag = false;
    }
}

不难看出来在这个程序中会出现变量多线程不可见的问题,在主线程最后把flag改为false时,t1线程无法感知到flag已经改变从而死循环运行结果如下:

在这里插入图片描述

如果给这个代码一些更改的话,在T类型中添加一个String然后在循环中不断访问这个String

package com.Xx.algorithm.test;
/**
 * @author XX
 * @date 2020/10/9 - 11:41
 */
public class V2 {
     

    private static class T{
     
        private boolean flag = true;
        String hello = new String("hello");
    }


    public static void main(String[] args) throws InterruptedException {
     
        T t = new T();
        Thread t1 = new Thread(new Runnable() {
     
            @Override
            public void run() {
     
                System.out.println("Start");
                while(t.flag){
     
                    System.out.println(t.hello);
                }
                System.out.println("end");
            }
        });
        t1.start();
        Thread.sleep(1000);
        t.flag = false;
    }
}

缓存行对齐以及MESI协议_第3张图片

这个程序就停止了,这便能说明在他访问这个String的时候一定有某种底层操作让他发现了flag的值发生了更改。其实就是因为缓存行的存在,两个线程在并发的过程中都拥有了String 和flag 他们在一个缓存行上,那么在每次获取String的时候就因为伪缓存的存在将flag的值改变了从而跳出了循环。

缓存行性能调优 能不能利用缓存行来提高程序性能呢?

package com.Xx.algorithm.test;

/**
 * @author XX
 * @date 2020/10/9 - 9:31
 */
public class V1 {
     

    public static class T{
     
//        public volatile long p1,p2,p3,p4,p5,p6,p7;
//        //8字节
        private volatile long x = 0L;
    }
    private static T[] arr = new T[2];

    static {
     
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {
     
        Thread thread1 = new Thread(()->{
     
            for(long i = 0;i < 1000_0000L;i++){
     
                //volatile的缓存一致性协议MESI或者锁总线,会消耗时间
                arr[0].x = i;
            }
        });
        Thread thread2 = new Thread(()->{
     
            for(long i = 0;i< 1000_0000L;i++){
     
                arr[1].x = i;
            }
        });
        long startTime = System.nanoTime();
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("总计消耗时间:"+(System.nanoTime()-startTime)/100_000);
    }
}

在没加上7个long的时候运行结果:

总计消耗时间:3361

如果加上7个long的时候运行结果:

总计消耗时间:981

当加上7个long,T对象的结构如图:

缓存行对齐以及MESI协议_第4张图片

那么一个T对象数组中arr[0]和arr[1]就不会在同一个缓存行中,一定是分开的两个缓存行因为是数组连续存储的结构前后必定是在不同的两个缓存行中,那么这两个值就不会发生伪共享的概念,那么在两个线程分别更改的过程中另一个就不会发生缓存同步的现象,如果发生了伪共享那么在一个线程更改值过后另一个线程会发生同步问题浪费时间。

MESI协议

​ 关于缓存一致性协议及其变种又是另一个繁杂的内容,而MESI其实仅仅是众多一致性协议中最著名的一个,其名字的得名也来至于该协议中对四种缓存状态的缩写简称,缓存一致性协议规定了如何保证缓存在各个CPU缓存的一致性问题:

以MESI协议为例,每个Cache line有4个状态,可用2个bit表示,它们分别是:

状态 描述
M(Modified) 这行数据有效,但数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。
E(Exclusive) 这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。
S(Shared) 这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中。
I(Invalid) 这行数据无效。

最简单的描述就是如果内存中的某个地址值中的数据发生改变了的话就会使其他拥有这个地址值数据副本的CPU收到同步请求进行缓存同步一致性,这是一种硬件级别的线程可见性原理实现。

参考

CPU高速缓存那些事儿

你可能感兴趣的:(缓存,java)