为什么要有缓存?
再说到cpu的缓存一致性前提下,需要了解到为什么会有缓存:
众所周知,cpu是用来计算和处理数据的,本身并不存储数据,数据都是放在内存或者硬盘中,而内存与cpu之间是存在距离的,如果说cpu目前要进行一次计算,那么就需要先从内存将数据取出再进行计算,而内存和cpu是存在速度差异的,在随着现代技术的发展,嵌入到cpu上的晶体管越来越多,那么就造成了cpu的计算和处理能力越来越强,这样就更加凸显了这个弊端,cpu在一次处理中更多的时间是在等待内存中的数据到来,这样的情况是我们人类是不能容忍的(我把你创造出来就是干活的,你可不能闲着);
在这样的需求驱动下,就需要提高cpu的利用率。思路就是在cpu和内存之间加一层缓冲(缓存,此处忽略三级缓存的差异,统称为缓存),因为cpu加载缓存中的数据比较快,所以就提高了cpu的利用率,那么现在cpu在处理数据的时候就会先从缓存中查找,找不到就去内存中找,但是这次cup从内存中不但是加载本次需要处理的数据,而且会将本次处理数据周围空间的数据也进行加载(这涉及时间局部性,空间局部性)因为cpu会觉得我下次要加载的数据肯定会在本次处理数据的周围,那么下次我就不需要再来读取内存了,直接在缓存中找就可以啦;
所以目前的架构变成了这个样子:
缓存的利和弊
现在cpu再加入了缓存之后如虎添翼,cpu的利用率大大提高了(基本实现了007);
■ 优点:加快了cpu的处理能力,提高了cpu的利用率
■ 缺点:
● 在单核cpu的的系统中,基本是没缺点的
● 那么在多核cpu的情况下会有什么问题呢?首先需要知道一个知识点(敲黑板啦),为了避免多个cpu共用一个缓存,造成多个cpu掐架,那么给每个cpu都分配一个女朋友,不对,是分配一个缓存,各自用各自的缓存,那么就会造成一个问题:数据不一致问题;
数据不一致问题
先来看一段代码:
下面的这段代码,执行后会发现,有一定概率 线程1 会一直在运行,
代码描述:
目前是开启了两个线程,那现在这两个线程各自由一个cpu所处理,而线程退出的条件是:公有变量COUNTER大于5,目前这个COUNTER值的增加是在 线程2中进行的;
执行流程:
- cpu1将线程1加载到自己的缓存,包括COUNTER 的值=0
- cpu2将线程2加载自己的缓存,包括COUNTER 的值=0
- 目前因为cpu1和cpu2将数据都各自拷贝了一份到自己的缓存中,那么cpu2对COUNTER的值进行增加操作,对于cpu1是无法感知的;所以cpu1会在while 一直出不来
解决:
对于这样因为缓存不一致导致的bug,在Java层面,我们可以使用 volatile 关键字来进行解决,这种在多cpu环境下变量的可见性问题,被volatile关键字标注的变量,当此变量的值被更改后,会强制将更改后的值刷新会内存,并失效各个cpu中的缓存的变量;
package com.zy.util;
import java.util.regex.Pattern;
/**
* @Author: hu.chen
* @Description:
* @DateTime: 2021/12/2 2:54 下午
**/
public class Test {
// volatile
private static int COUNTER = 0;
public static void main(String[] args) {
new ChangeListener().start();
new ChangeMaker().start();
}
static class ChangeListener extends Thread {
@Override
public void run() {
int threadValue = COUNTER;
while (threadValue < 5) {
if (threadValue != COUNTER) {
System.out.println("线程1 : " + COUNTER + "");
threadValue = COUNTER;
}
}
}
}
static class ChangeMaker extends Thread {
@Override
public void run() {
int threadValue = COUNTER;
while (COUNTER < 5) {
System.out.println("线程2 : " + (threadValue + 1) + "");
COUNTER = ++threadValue;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
操作系统级别的缓存一致性解决方案
解决缓存不一致性问题,通常来说有以下2种解决方法:
- 通过在总线加LOCK#锁的方式
- 通过缓存一致性协议
这2种方式都是硬件层面上提供的方式
总线加lock锁:
在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。
但是这种方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下;
缓存一致性协议:
缓存一致性协议中,最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取
在MESI协议中,每个cache line有4个状态,可用2个bit表示,它们分别是
状态 | 描述 |
---|---|
M(Modified) | 这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 |
E(Exclusive) | 这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。 |
S(Shared) | 这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中。 |
I(Invalid) | 这行数据无效 |