java多线程和高并发系列二 & 缓存一致性协议MESI

2023-11-17

目录

CPU高速缓存(Cache Memory)

CPU为何要有高速缓存

带有高速缓存的CPU执行计算的流程

目前流行的多级缓存结构

多核CPU多级缓存一致性协议MESI

MESI协议缓存状态

MESI状态转换

多核缓存协同操作

单核读取

双核读取

修改数据

同步数据

缓存行伪共享

什么是伪共享?

怎么解决伪共享?

MESI优化和他们引入的问题

CPU切换状态阻塞解决-存储缓存(Store Bufferes)

Store Bufferes

Store Bufferes的风险

硬件内存模型

java多线程和高并发系列三 & Synchronized锁详解


CPU高速缓存(Cache Memory)

CPU为何要有高速缓存

CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题。

在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。

时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。

比如循环、递归、方法的反复调用等。

空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。

比如顺序执行的代码、连续创建的两个对象、数组等。

带有高速缓存的CPU执行计算的流程

  1. 程序以及数据被加载到主内存
  2. 指令和数据被加载到CPU的高速缓存
  3. CPU执行指令,把结果写到高速缓存
  4. 高速缓存中的数据写回主内存

目前流行的多级缓存结构

由于CPU的运算速度超越了1级缓存的数据I\O能力,CPU厂商又引入了多级的缓存结构。

多核CPU多级缓存一致性协议MESI

多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。

MESI协议缓存状态

MESI 是指4种状态的首字母。每个Cache line有4个状态,可用2个bit表示,它们分别是:

缓存行(Cache line):缓存存储数据的单元

状态

描述

监听任务

M 修改 (Modified)

该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。

缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。

E 独享、互斥 (Exclusive)

该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。

缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。

S 共享 (Shared)

该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。

缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。

I 无效 (Invalid)

该Cache line无效。

注意:

对于M和E状态而言总是精确的,他们在和该缓存行的真正状态是一致的,而S状态可能是非一致的。如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将该缓存行升迁为E状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。

从上面的意义看来E状态是一种投机性的优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy变成invalid状态,而修改E状态的缓存不需要使用总线事务。

MESI状态转换

理解该图的前置说明:

  1. 触发事件
  2. 触发事件

    描述

    本地读取(Local read)

    本地cache读取本地cache数据

    本地写入(Local write)

    本地cache写入本地cache数据

    远端读取(Remote read)

    其他cache读取本地cache数据

    远端写入(Remote write)

    其他cache写入本地cache数据

  3. cache分类

前提:所有的cache共同缓存了主内存中的某一条数据。

本地cache:指当前cpu的cache。

触发cache:触发读写事件的cache。

其他cache:指既除了以上两种之外的cache。

注意:本地的事件触发 本地cache和触发cache为相同。

上图的切换解释:

状态

触发本地读取

触发本地写入

触发远端读取

触发远端写入

M状态(修改)

本地cache:M

触发cache:M

其他cache:I

本地cache:M

触发cache:M

其他cache:I

本地cache:M→E→S

触发cache:I→S

其他cache:I→S

同步主内存后修改为E独享,同步触发、其他cache后本地、触发、其他cache修改为S共享

本地cache:M→E→S→I

触发cache:I→S→E→M

其他cache:I→S→I

同步和读取一样,同步完成后触发cache改为M,本地、其他cache改为I

E状态(独享)

本地cache:E

触发cache:E

其他cache:I

本地cache:E→M

触发cache:E→M

其他cache:I

本地cache变更为M,其他cache状态应当是I(无效)

本地cache:E→S

触发cache:I→S

其他cache:I→S

当其他cache要读取该数据时,其他、触发、本地cache都被设置为S(共享)

本地cache:E→S→I

触发cache:I→S→E→M

其他cache:I→S→I

当触发cache修改本地cache独享数据时时,将本地、触发、其他cache修改为S共享.然后触发cache修改为独享,其他、本地cache修改为I(无效),触发cache再修改为M

S状态(共享)

本地cache:S

触发cache:S

其他cache:S

本地cache:S→E→M

触发cache:S→E→M

其他cache:S→I

当本地cache修改时,将本地cache修改为E,其他cache修改为I,然后再将本地cache为M状态

本地cache:S

触发cache:S

其他cache:S

本地cache:S→I

触发cache:S→E→M

其他cache:S→I

当触发cache要修改本地共享数据时,触发cache修改为E(独享),本地、其他cache修改为I(无效),触发cache再次修改为M(修改)

I状态(无效)

本地cache:I→S或者I→E

触发cache:I→S或者I →E

其他cache:E、M、I→S、I

本地、触发cache将从I无效修改为S共享或者E独享,其他cache将从E、M、I 变为S或者I

本地cache:I→S→E→M

触发cache:I→S→E→M

其他cache:M、E、S→S→I

既然是本cache是I,其他cache操作与它无关

既然是本cache是I,其他cache操作与它无关

下图示意了,当一个cache line的调整的状态的时候,另外一个cache line 需要调整的状态。

M

E

S

I

M

×

×

×

E

×

×

×

S

×

×

I

举个栗子来说:

假设cache 1 中有一个变量x = 0的cache line 处于S状态(共享)。

那么其他拥有x变量的cache 2、cache 3等x的cache line调整为S状态(共享)或者调整为 I 状态(无效)。

多核缓存协同操作

假设有三个CPU A、B、C,对应三个缓存分别是cache a、b、 c。在主内存中定义了x的引用值为0。

单核读取

那么执行流程是:

CPU A发出了一条指令,从主内存中读取x。

从主内存通过bus读取到缓存中(远端读取Remote read),这是该Cache line修改为E状态(独享).

双核读取

那么执行流程是:

CPU A发出了一条指令,从主内存中读取x。

CPU A从主内存通过bus读取到 cache a中并将该cache line 设置为E状态。

CPU B发出了一条指令,从主内存中读取x。

CPU B试图从主内存中读取x时,CPU A检测到了地址冲突。这时CPU A对相关数据做出响应。此时x 存储于cache a和cache b中,x在cache a和cache b中都被设置为S状态(共享)。

修改数据

那么执行流程是:

CPU A 计算完成后发指令需要修改x.

CPU A 将x设置为M状态(修改)并通知缓存了x的CPU B, CPU B将本地cache b中的x设置为I状态(无效)

CPU A 对x进行赋值。

同步数据

那么执行流程是:

CPU B 发出了要读取x的指令。

CPU B 通知CPU A,CPU A将修改后的数据同步到主内存时cache a 修改为E(独享)

CPU A同步CPU B的x,将cache a和同步后cache b中的x设置为S状态(共享)。

缓存行伪共享

什么是伪共享?

CPU缓存系统中是以缓存行(cache line)为单位存储的。目前主流的CPU Cache 的 Cache Line 大小都是64Bytes。在多线程情况下,如果需要修改“共享同一个缓存行的变量”,就会无意中影响彼此的性能,这就是伪共享(False Sharing)。

举个例子: 现在有2个long 型变量 a 、b,如果有t1在访问a,t2在访问b,而a与b刚好在同一个cache line中,此时t1先修改a,将导致b被刷新!

怎么解决伪共享?

Java8中新增了一个注解:@sun.misc.Contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置 -XX:-RestrictContended才会生效。

@sun.misc.Contended 
public final static class TulingVolatileLong { 
    public volatile long value = 0L; 
    //public long p1, p2, p3, p4, p5, p6; 
} 

MESI优化和他们引入的问题

缓存的一致性消息传递是要时间的,这就使其切换时会产生延迟。当一个缓存被切换状态时其他缓存收到消息完成各自的切换并且发出回应消息这么一长串的时间中CPU都会等待所有缓存响应完成。可能出现的阻塞都会导致各种各样的性能问题和稳定性问题。

CPU切换状态阻塞解决-存储缓存(Store Bufferes)

比如你需要修改本地缓存中的一条信息,那么你必须将I(无效)状态通知到其他拥有该缓存数据的CPU缓存中,并且等待确认。等待确认的过程会阻塞处理器,这会降低处理器的性能。应为这个等待远远比一个指令的执行时间长的多。

Store Bufferes

为了避免这种CPU运算能力的浪费,Store Bufferes被引入使用。处理器把它想要写入到主存的值写到缓存,然后继续去处理其他事情。当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。

这么做有两个风险

Store Bufferes的风险

  1. 处理器会尝试从存储缓存(Store buffer)中读取值,但它还没有进行提交。这个的解决方案称为Store Forwarding,它使得加载的时候,如果存储缓存中存在,则进行返回。
  2. 保存什么时候会完成,这个并没有任何保证。
value = 3;
void exeToCPUA(){
  value = 10;
  isFinsh = true;
}
void exeToCPUB(){
  if(isFinsh){
    //value一定等于10?!
    assert value == 10;
  }
}

试想一下开始执行时,CPU A保存着finished在E(独享)状态,而value并没有保存在它的缓存中。(例如,Invalid)。在这种情况下,value会比finished更迟地抛弃存储缓存。完全有可能CPU B读取finished的值为true,而value的值不等于10。

即isFinsh的赋值在value赋值之前。

这种在可识别的行为中发生的变化称为重排序(reordings)。注意,这不意味着你的指令的位置被恶意(或者好意)地更改。

它只是意味着其他的CPU会读到跟程序中写入的顺序不一样的结果。

顺便提一下NIO的设计和Store Bufferes的设计是非常相像的。

硬件内存模型

执行失效也不是一个简单的操作,它需要处理器去处理。另外,存储缓存(Store Buffers)并不是无穷大的,所以处理器有时需要等待失效确认的返回。这两个操作都会使得性能大幅降低。为了应付这种情况,引入了失效队列。它们的约定如下:

  • 对于所有的收到的Invalidate请求,Invalidate Acknowlege消息必须立刻发送
  • Invalidate并不真正执行,而是被放在一个特殊的队列中,在方便的时候才会去执行。
  • 处理器不会发送任何消息给所处理的缓存条目,直到它处理Invalidate。

即便是这样处理器已然不知道什么时候优化是允许的,而什么时候并不允许。干脆处理器将这个任务丢给了写代码的人。这就是内存屏障(Memory Barriers)。

写屏障 Store Memory Barrier(a.k.a. ST, SMB, smp_wmb)是一条告诉处理器在执行这之后的指令之前,应用所有已经在存储缓存(store buffer)中的保存的指令。

读屏障Load Memory Barrier (a.k.a. LD, RMB, smp_rmb)是一条告诉处理器在执行任何的加载前,先应用所有已经在失效队列中的失效操作的指令。

void executedOnCpu0() {
    value = 10;
    //在更新数据之前必须将所有存储缓存(store buffer)中的指令执行完毕。
    storeMemoryBarrier();
    finished = true;
}
void executedOnCpu1() {
    while(!finished);
    //在读取之前将所有失效队列中关于该数据的指令执行完毕。
    loadMemoryBarrier();
    assert value == 10;
}


java多线程和高并发系列三 & Synchronized锁详解

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

java多线程和高并发系列二 & 缓存一致性协议MESI 的相关文章

随机推荐

  • Aviator 表达式的使用

    1 使用Aviator 需要导入包
  • 计算机视觉人体骨骼点动作识别-1.训练自己的关键点检测模型

    人体关键点检测算法 关键点并不特指人体骨骼关键点 还有人脸关键点 物体的关键点 其中人体的关键点 也叫作pose Estimation 是最热门 也是最有难度 应用最广的 应用可以包括 行为识别 人机交互 智能家居 虚拟现实 具体细分下来可
  • USB之基础知识

    1 USB概述 USB Universal Serial Bus 通用串行总线 是一个外部总线标准 用于规范电脑与外部设备的连接和通讯 USB接口支持设备的即插即用和热插拔功能 USB接口有4个pin脚 分别为VCC GND Data Da
  • 通过tomcat.mamager页面远程管理tomcat

    通过java访问tomcat的manager来管理 此方法无法达到重启tomcat的目的 只能重启应用 先在tomcat中修改tomcat users xml
  • 嵌入式Linux编译系统的设计——Bootloader, 内核,驱动,文件系统,升级镜像等自动化编译打包

    项目简介 嵌入式系统的开发过程较为复杂 编译 裁剪 定制等如果没有一套规范的流程将会难于管理和控制 本项目的目的是设计一个嵌入式Linux编译系统 实现代码的编译 定制和裁剪 Bootloader 内核 驱动 文件系统 升级镜像等都可以自动
  • 前端将静态页面放在移动端上,查看效果

    想要将本地刚写完的静态HTML文件 放在移动端上查看 不是放在浏览器中 打开移动端模式 需要进行一下步骤 1 全局安装 node 具体步骤看官网 2 在全局运行 cmd 输入 npm install anywhere g 3 打开静态资源
  • 前端例程20220920:纯CSS图片自动轮播效果

    演示 原理 代码
  • tp5实现短信注册,调用第三方接口,电话通知,和短信通知都可以。

    thinkphp5实现短线验证注册 思路 1 一个表单 表单中要有一个手机字段 和密码 2 在提交验证码添加点击事件 触发ajax 请求后台 发送短信 3 后台中编写控制器方法 一个ajax请求发送短信 一个表单验证成功条页面 4 在con
  • OpenCV copyTo操作会覆盖原数据

    前言 有一些指导说copyTo只会覆盖对应区域 这边做了一个测试 测试 测试代码 int main Mat photo1 imread home evening 桌面 5 png if photo1 data cout lt lt erro
  • Your account has been blocked. git更新代码时报错

    记录一下问题 原因解释 当前用户登录信息过期 在浏览器里登录gitlab后台 长时间没有重新登录 导致后端登录session失效 自动锁住用户账号 此时通过ssh下载也就无法更新代码 解决办法 在浏览器重新登录gitlab网站 然后重新更新
  • 【tomcat】应用服务

    准备环境 三台虚拟机 192 168 1 120 192 168 1 122 192 168 1 131 三台虚拟机关闭防火墙 查看光盘 检测yun创库 查看JDK是否安装 root localhost java version openj
  • 蓝桥杯 - 负载均衡

    输入样例 2 6 5 5 1 1 5 3 2 2 2 6 3 1 2 3 4 1 6 1 5 1 3 3 6 1 3 4 输出样例 2 1 1 1 1 0 解析 优先队列 排序规则为任务结束的时间 在新任务的时候 弹出已经结束的任务 并且恢
  • 什么是自动与自主?

    自动与自主的区别很有意思 平时大家都不爱斟酌 一般都是拿过来就用 岂不知 西方人常常不是这样子的 他们一般先从基本概念上进行咬文嚼字般的抠 然后在此基础上进行理论过程的推导演算或实验实践的验证分析 于是差距往往就此拉开 自主 自建构 系统中
  • virtio-netdev 发送数据包

    在前面几文中已经大体介绍了virtio的重要组成 包含virtio net设备的创建 vring的创建 与virtio设备的交互方式 我们就从网络数据包的发送角度来看下virtio的详细使用流程 点击查看全文 http luoye me 2
  • 网络编程1

    网络编程 网络编程是Java最擅长的方向之一 使用Java进行网络编程时 由虚拟机实现了底层复杂的网络协议 Java程序只需要调用Java标准库提供的接口 就可以简单高效地编写网络程序 1 前置知识点 学习网络编程之前 我们需要先了解什么是
  • HttpRunner 3.x接口自动化: 全面讲解如何落地实战

    今天 我们来一起学习下HttpRunner3 主要讲解如何使用 应用技巧 基本知识点总结和需要注意事项 一篇文章没法面面俱到 如果有重要的地方没写到 可以给我留言 咱们接着补充 整体概览 概述介绍 HttpRunner 是一款面向 HTTP
  • VSCODE远程ssh调试linux+cpolar内网穿透

    VSCODE远程ssh调试linux cpolar内网穿透 一 cpolar配置与使用 1 进入cpolar官网https i cpolar com m 4kqU 2 点击免费使用 进行账号注册 3 linux安装cpolar 国内安装 c
  • js事件循环,根据W3C最新说明

    事件循环 消息循环 首先我们js是一门单线程的语言 这是因为它运行在浏览器的渲染主线程中 而主线程只有一个 那么渲染主线程是如何工作的 渲染主线程是浏览器中最繁忙的线程 需要它处理的任务包括但不限于 解析 HTML 解析 CSS 计算样式
  • QT中日期和时间类

    QT中日期和时间类 QDate QTime QDateTime QDate QDate类可以封装日期信息也可以通过这个类得到日期相关的信息 包括 年 月 日 构造函数 QDate QDate QDate QDate int y int m
  • java多线程和高并发系列二 & 缓存一致性协议MESI

    目录 CPU高速缓存 Cache Memory CPU为何要有高速缓存 带有高速缓存的CPU执行计算的流程 目前流行的多级缓存结构 多核CPU多级缓存一致性协议MESI MESI协议缓存状态 MESI状态转换 多核缓存协同操作 单核读取 双