关于java:看懂这篇才能说了解并发底层技术

0次阅读

共计 6260 个字符,预计需要花费 16 分钟才能阅读完成。

零、开局

前两天我搞了两个每日一个知识点,对多线程并发的局部常识做了下概括性的总结。但通过小伙伴的反馈是,那玩意写的比拟形象,看的云里雾里晕晕乎乎的。

所以又针对多线程底层这一块再从新做下系统性的解说。
有趣味的敌人能够先看下前两节,能够说是个抽象的概念版。

好了,回归正题。在多线程并发的世界里 synchronized、volatile、JMM 是咱们绕不过来的技术坎,而重排序、可见性、内存屏障又有时候搞得你一脸懵逼。有道是知其然知其所以然,理解了底层的原理性问题,不论是日常写 BUG 还是面试都是必备神器了。

先看几个问题点:

1、处理器与内存之间是怎么交互的?

2、什么是缓存一致性协定?

3、高速缓存内的音讯是怎么更新变动的?

4、内存屏障又和他们有什么关系?

如果下面的问题你都能滚瓜烂熟,那就去看看电影放松下吧!

一、高速缓存

目前的处理器的解决能力要远远的胜于主内存(DRAM)拜访的效率,往往主内存执行一次读写操作所需的工夫足够处理器执行上百次指令。所以为了填补处理器与主内存之间的差距,设计者们在主内存和处理器间接引入了高速缓存(Cache)。如图:

其实在古代处理器中,会有多级高速缓存。个别咱们会成为一级缓存(L1 Cache)、二级缓存(L2 Cache)、三级缓存(L3 Cache)等,其中一级缓存个别会被集成在 CPU 内核中。如图:

内部结构

高速缓存存在于每个处理器内,处理器在执行读、写操作的时候并不需要间接与内存交互,而是通过高速缓存进行。

高速缓存内其实就是为应用程序拜访的变量保留了一个数据正本。高速缓存相当于一个容量极小的散列表(Hash Table),其键是一个内存地址,值是内存数据的正本或是咱们筹备写入的数据。从其外部来看,其实相当于一个拉链散列表,也就是蕴含了很多桶,每个桶上又能够蕴含很多缓存条目(想想 HashMap),如图:

缓存条目

在每个缓存条目中,其实又蕴含了 Tag、Data Block、Flag 三个局部,咱们来个小图:

  • Data Block : 也就是咱们经常叨叨的缓存行(Cache Line), 她其实是高速缓存与主内存间进行数据交互的最小单元,外面存储着咱们须要的变量数据。
  • Tag : 蕴含了缓存行中数据内存地址的信息(其实是内存地址的高位局部的比特)
  • Flag : 标识了以后缓存行的状态(MESI 咯)

那么,咱们的处理器又是怎么寻找到咱们须要的变量呢?

不多说,上图:

其实,在处理器执行内存拜访变量的操作时,会对内存地址进行解码的(由高速缓存控制器执行)。而解码后就会失去 tag、index、offset 三局部数据。

index : 咱们晓得高速缓存内的构造是一个拉链散列表,所以 index 就是为了帮咱们来定位到底是哪个缓存条目标。

tag : 很显著和咱们缓存条目中的 Tag 一样,所以 tag 相当于缓存条目标编号。次要用于,在同一个桶下的拉链中来寻找咱们的指标。

offset : 咱们要晓得一个前提,就是一个缓存条目中的缓存行是能够存储很多变量的,所以 offset 的作用是用来确定一个变量在缓存行中的起始地位。

所以,在如果在高速缓存内能找到缓存条目并且定位到了响应得缓存行,而此时缓存条目标 Flag 标识为无效状态,这时候也就是咱们所说的缓存命中(Cache Hit), 否则就是缓存未命中(Cache Miss)。

缓存未命有包含读未命中(Read Miss)和写未命中 (Write Miss) 两种, 对应着对内存的读写操作。

而在读未命中(Read Miss) 产生时,处理器所须要的数据会从主内存加载并被存入高速缓存对应的缓存行中,此过程会导致处理器进展(Stall)而不能执行其余指令。

二、缓存一致性协定

在多线程进行共享变量拜访时,因为各个线程执行的处理器上的高速缓存中都会保留一份变量的正本数据,这样就会有一个问题,那当一个正本更新后怎么保障其它处理器能马上的获取到最新的数据。这其实就是缓存一致性的问题,其本质也就是怎么避免数据的脏读。

为了解决这个问题,处理器间呈现了一种通信机制,也就是缓存一致性协定(Cache Coherence Protocol)。

MESI 是什么

缓存一致性协定有很多种,MESI(Modified-Exclusive-Shared-Invalid)协定其实是目前应用很宽泛的缓存一致性协定,x86 处理器所应用的缓存一致性协定就是基于 MESI 的。

咱们能够把 MESI 对内存数据拜访了解成咱们罕用的读写锁,它能够使对同一内存地址的读操作是并发的,而写操作是独占的。所以在任何时刻写操作只能有一个处理器执行。而在 MESI 中,一个处理器要向内存写数据时必须持有该数据的所有权。

MESI 将缓存条目标状态分为了 Modified、Exclusive、Shared、Invalid 四种,并在此基础上定义了一组音讯用于处理器的读、写内存操作。如图:

MESI 的四种状态

所以 MESI 其实就是应用四种状态来标识了缓存条目以后的状态,来保障了高速缓存内数据一致性的问题。那咱们来认真的看下四种状态

Modified:

示意高速缓存中相应的缓存行内的数据曾经被更新了。因为 MESI 协定中任意时刻只能有一个处理器对同一内存地址对应的数据进行更新,也就是说再多个处理器的高速缓存中雷同 Tag 值得缓存条目只能有一个处于 Modified 状态。处于此状态的缓存条目中缓存行内的数据与主内存蕴含的数据不统一。

Exclusive:

示意高速缓存相应的缓存行内的数据正本与主内存中的数据一样。并且,该缓存行以独占的形式保留了相应主内存地址的数据正本,此时其余解决上高速缓存以后都不保留该数据的无效正本。

Shared:

示意以后高速缓存相应缓存行蕴含相应主内存地址对应的数据正本,且与主内存中的数据是统一的。如果缓存条目状态是 Shared 的,那么其余处理器上如果也存在雷同 Tag 的缓存条目,那这些缓存条目状态必定也是 Shared。

Invalid:

示意该缓存行中不蕴含任何主内存中的无效数据正本,这个状态也是缓存条目标初始状态。

MESI 解决机制

后面说了那么多,都是 MESI 的基础理论,那么,MESI 协定到底是怎么来协调处理器进行内存的读写呢?

其实,想协调解决必然须要先和各个处理器进行通信。所以 MESI 协定定义了一组音讯机制用于协调各个处理器的读写操作。

咱们能够参考 HTTP 协定来进行了解,能够将 MESI 协定中的音讯分为申请和响应两类。处理器在进行主内存读写的时候会往总线(Bus)中发申请音讯,同时每个处理器还会嗅探(Snoop)总线中由其余处理器收回的申请音讯并在肯定条件下往总线中回复响应得响应音讯。

针对于音讯的类型,有如下几种:

  • Read : 申请音讯,用于告诉其余处理器、主内存,以后处理器筹备读取某个数据。该音讯内蕴含待读取数据的主内存地址。
  • Read Response: 响应音讯,该音讯内蕴含了被申请读取的数据。该音讯可能是主内存返回的,也可能是其余高速缓存嗅探到 Read 音讯返回的。
  • Invalidate: 申请音讯,告诉其余处理器删除指定内存地址的数据正本。其实就是通知他们你这个缓存条目内的数据有效了,删除只是逻辑上的,其实就是更新下缓存条目标 Flag.
  • Invalidate Acknowledge: 响应音讯,接管到 Invalidate 音讯的处理器必须回复此音讯,示意曾经删除了其高速缓存内对应的数据正本。
  • Read Invalidate: 申请音讯,此音讯为 Read 和 Invalidate 音讯组成的复合音讯,作用次要是用于告诉其余处理器以后处理器筹备更新一个数据了,并申请其余处理器删除其高速缓存内对应的数据正本。接管到该音讯的处理器必须回复 Read Response 和 Invalidate Acknowledge 音讯。
  • Writeback: 申请音讯,音讯蕴含了须要写入主内存的数据和其对应的内存地址。

理解完了根底的音讯类型,那么咱们就来看看 MESI 协定是如何帮助处理器实现内存读写的,看图谈话:

举例:如果内存地址 0xxx 上的变量 s 是 CPU1 和 CPU2 共享的 咱们先来说下 CPU 上读取数据 s

高速缓存内存在无效数据时:

CPU1 会依据内存地址 0xxx 在高速缓存找到对应的缓存条目,并读取缓存条目标 Tag 和 Flag 值。如果此时缓存条目标 Flag 是 M、E、S 三种状态的任何一种,那么就间接从缓存行中读取地址 0xxx 对应的数据,不会向总线中发送任何音讯。

高速缓存内不存在无效数据时:

1、如 CPU2 高速缓存内找到的缓存条目状态为 I 时,则阐明此时 CPU2 的高速缓存中不蕴含数据 s 的无效数据正本。

2、CPU2 向总线发送 Read 音讯来读取地址 0xxx 对应的数据 s.

3、CPU1(或主内存)嗅探到 Read 音讯,则须要回复 Read Response 提供相应的数据。

4、CPU2 接管到 Read Response 音讯时,会将其中携带的数据 s 存入相应的缓存行并将对应的缓存条目状态更新为 S。

从宏观的角度看,就是下面的流程了,咱们再持续深刻下,看看在缓存条目为 I 的时候到底是怎么进行音讯解决的

说完了读取数据,咱们就 在说 下 CPU1 是怎么写入一个地址为 0xxx 的数据 s 的

MESI 协定解决了缓存一致性的问题,但其中有一个问题,那就是须要在期待其余处理器全副回复后能力进行下一步操作,这种期待显著是不能承受的,上面就持续来看看大神们是怎么解决处理器期待的问题的。

三、写缓冲和有效化队列

因为 MESI 本身有个问题,就是在写内存操作的时候必须期待其余所有处理器将本身高速缓存内的相应数据正本都删除后,并接管到这些处理器回复的 Invalidate Acknowledge/Read Response 音讯后能力将数据写入高速缓存。

为了防止这种期待造成的写操作提早,硬件设计引入了写缓冲器和有效化队列。

写缓冲器(Store Buffer)

在每个处理器内都有本人独立的写缓冲器,写缓冲器外部蕴含很多条目(Entry), 写缓冲器比高速缓存还要小点。

那么,在引入了写缓冲器后,处理器在执行写入数据的时候会做什么解决呢?还会间接发送音讯到 BUS 吗?

咱们来看几个场景:

(留神 x86 处理器是不论相应的缓存条目是什么状态,都会间接将每一个写操作后果存入写缓冲器)

1、如果此时缓存条目状态是 E 或者 M:

代表此时处理器曾经获取到数据所有权,那么就会将数据间接写入相应的缓存行内,而不会向总线发送音讯。

2、如果此时缓存条目状态是 S

  • 此时处理器会将写操作的数据存入写缓冲器的条目中,并发送 Invalidate 音讯。
  • 如果此时相应缓存条目标状态是 I,那就称之为写操作遇到了写未命中(Write Miss),此时就会将数据先写入写缓冲器的条目中,而后在发送 Read Invalidate 来告诉其余处理器我要进行数据更新了。
  • 处理器的写操作其实在将数据写入缓冲器时就实现了,处理器并不需要期待其余处理器返回 Invalidate Acknowledge/Read Response 音讯
  • 当处理器接管到其余处理器回复的针对于同一个缓存条目标 Invalidate Acknowledge 音讯时,就会将写缓冲内对应的数据写入相应的缓存行中

通过下面的场景形容咱们能够看出,写缓冲器帮忙处理器实现了异步写数据的能力,使得处理器解决指令的能力大大晋升。

有效化队列(Invalidate Queue)

其实在处理器接到 Invalidate 类型的音讯时,并不会删除音讯中指定地址对应的数据正本(也就是说不会去马上批改缓存条目标状态为 I),而是将音讯存入有效化队列之后就回复 Invalidate Acknowledge 音讯了,次要起因还是为了缩小处理器期待的工夫。

所以不论是写缓冲器还是有效化队列,其实都是为了缩小处理器的等待时间,采纳了空间换工夫的形式来实现命令的异步解决。

总之就是,写缓冲器解决了写数据时要期待其余处理器响应得问题,有效化队列帮忙解决了删除数据期待的问题。

但既然是异步的,那必然又会带来新的问题 — 内存重排序和可见性问题。

所以,咱们持续接着聊。

存储转发(Store Fowarding)

通过下面内容咱们晓得了有了写缓冲器后,处理器在写数据时间接写入缓冲器就间接返回了。

那么问题就来了,当咱们写完一个数据又要马上进行读取可咋办呢?话不多说,咱们还是举个例子来说,如图:

此时第一步处理器将变量 S 的更新后的数据写入到写缓冲器返回,接着马上执行了第二布进行 S 变量的读取。因为此时处理器对 S 变量的更新后果还停留在写缓冲器中,因而从高速缓存缓存行中读到的数据还是变量 S 的旧值。

为了解决这种问题,存储转发(Store Fowarding)这个概念上线了。其实践就是处理器在执行读操作时会先依据相应的内存地址从写缓冲器中查问。如果查到了间接返回,否则处理器才会从高速缓存中查找,这种从缓冲器中读取的技术就叫做存储转发。看图:

内存重排序和可见性的问题

因为写缓冲器和有效化队列的呈现,处理器的执行都变成了异步操作。缓冲器是每个处理器公有的,一个处理器所存储的内容是无奈被其余处理器读取的。

举个例子:

CPU1 更新变量到缓冲器中,而 CPU2 因为无奈读取到 CPU1 缓冲器内容所以从高速缓存中读取的依然是该变量旧值。

其实这就是写缓冲器导致 StoreLoad 重排序问题,而写缓冲器还会导致 StoreStore 重排序问题等。

为了使一个处理器上运行的线程对共享变量所做的更新被其余处理器上运行的线程读到,咱们必须将写缓冲器的内容写到其余处理器的高速缓存上,从而使在缓存一致性协定作用下此次更新能够被其余处理器读取到。

处理器在写缓冲器满、I/ O 指令被执行时会将写缓冲器中的内容写入高速缓存中。但从变量更新角度来看,处理器自身无奈保障这种更新的”及时“性。为了保障处理器对共享变量的更新可被其余处理器同步,编译器等底层零碎借助一类称为内存屏障的非凡指令来实现。

内存屏障中的存储屏障(Store Barrier)会使执行该指令的处理器将写缓冲器内容写入高速缓存。

内存屏障中的加载屏障(Load Barrier)会依据有效化队列内容指定的内存地址,将相应处理器上的高速缓存中相应的缓存条目状态标记为 I。

四、内存屏障

因为说了存储屏障(Store Barrier)和加载屏障(Load Barrier),所以这里再简略的提下内存屏障的概念。

划重点:(你细品)

处理器反对哪种内存重排序(LoadLoad 重排序、LoadStore 重排序、StoreStore 重排序、StoreLoad 重排序),就会提供绝对应可能禁止重排序的指令,而这些指令就被称之为 内存屏障(LoadLoad 屏障、LoadStore 屏障、StoreStore 屏障、StoreLoad 屏障)

划重点:

如果用 X 和 Y 来代替 Load 或 Store, 这类指令的作用就是禁止该指令左侧的任何 X 操作与该指令右侧的任何 Y 操作之间进行重排序(就是替换地位),确保指令左侧的所有 X 操作都优先于指令右侧的 Y 操作。

内存屏障的具体作用:

五、总结

其实从头看到尾就会发现,一个技术点的呈现往往是为了填补另一个的坑。

为了解决处理器与主内存之间的速度鸿沟,引入了高速缓存,却又导致了缓存一致性问题

为了解决缓存一致性问题,引入了如 MESI 等技术,又导致了处理器期待问题

为了解决处理器期待问题,引入了写缓冲和有效化队列,又导致了重排序和可见性问题

为了解决重排序和可见性问题,引入了内存屏障,舒坦。。。

欢送关注公众号【码农开花】一起学习成长
我会始终分享 Java 干货,也会分享收费的学习材料课程和面试宝典
回复:【计算机】【设计模式】【面试】有惊喜哦

正文完
 0