关于java:硬件基础和java内存模型

3次阅读

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

  • 高速缓存
  • 缓存一致性协定
  • 写缓冲器和有效化队列
  • 存储转发
  • 内存重排序
  • 可见性问题
  • 根本内存屏障
  • 同步机制和内存屏障
  • 虚拟机对内存屏障的优化

高速缓存

  1. 当初处理器的解决能力要远超于主内存的拜访速率, 一次主内存的读或写操作所须要的工夫足够处理器执行几百条指令, 为了补救处理器和主内存解决能力之间的鸿沟, 便在处理器和主内存之间引进了高速缓存.

    高速缓存是一种读取速率远超主内存, 然而容量远小于主内存的一种的一种存储部件, 每个处理器都领有本人的高速缓存.

  2. 在高速缓存中, 相当于为拜访程序的每一个变量存储一个正本, 变量名为相当于内存地址, 变量值相当于相应内存空间所存储的数据, 然而高速缓存中并不每时每刻蕴含所有变量的正本.
  3. 高速缓存相当于用硬件实现的一个散列表 (拉链法), 其键(key) 是一个内存地址, 其值 (value) 是数据正本或要写入内存的数据. 高速缓存的构造大抵如下:

    高速缓存是一个经典的拉链散列表构造, 高速缓存蕴含若干桶, 每个桶前面又蕴含若干个缓存条目, 而一个缓存条目又能够分为:Tag Data-BlockFlag三局部,Tag是用了来辨别数据是在那个数据条目上,Flag是用来标识这个数据条目标状态.Data-Block也被叫做数据行是用来存储从主内存读取的数据或筹备写入主内存的数据, 一个数据行可能蕴含若干个变量的值.

    当处理器筹备读取一条数据时, 处理器会将相应的内存地址 ” 解码 ” 失去三个值:

    • index – 用于确定桶编号
    • tag – 用于确定缓存条目
    • offset – 用于确定数据所在条目中的偏移量

    若依据内存地址在高速缓存中找到这个数据就被称为缓存命中, 否则处理器将会去主内存中去查找这条数据.

  4. 当初处理器个别会具备多个档次的高速缓存, 相应的是 一级缓存 (L1 Cache) 二级缓存(L2 Cache) 三级缓存(L3 Cache) 等, 个别一级缓存会被集成在处理器内核中, 因而其访问速度十分高, 个别状况下以及缓存的读取会在 2 - 4 个处理器时钟循环内实现, 其中一部分用于存储指令(L1i), 一部分用于存储数据(L1d), 个别越凑近处理器外围的高速缓存, 存取速率越高, 制作老本越高, 容量越小.

    在 linux 零碎能够应用 lscpu 命令查看缓存档次

缓存一致性协定

  1. MESI(Modified-Exclusive-Shared-Invalid)是一种广为应用的缓存一致性协定,X86 处理器的缓存一致性协定就是基于 MESI 协定的, 它对于拜访的管制相似于读写锁, 即对于同一地址的读操作时并发的, 对于同一地址的写操作是独占的.

    MESI 协定将缓存条目标状态分为四种: Modified Exclusive Shared Invalid

    • Invalid(有效的, 记为 I): 示意相应缓存行中 不蕴含 任何内存地址所对应的数据正本, 该状态是缓存条目标初始状态.
    • Shared(共享的, 记为 S): 示意相应的缓存行 蕴含 相应的内存地址所对应的数据正本, 并且, 其它处理器上的高速缓存中也可能有雷同的内存地址对应的数据正本. 所以一个缓存条目标状态是 Shared, 并且, 如果其它处理器上也存在与这个给处理器雷同的 tag 的缓存条目, 那么这些缓存条目标状态也为 Shared. 处于这个状态的缓存条目, 其缓存行中蕴含的数据是与主内存中蕴含的数据是 统一的.
    • Exclusive(独占的, 记为 E): 示意相应缓存行 蕴含 相应的内存地址对应的数据正本, 并且, 缓存行以独占的形式保留了内存地址所对应的数据正本, 即, 其它处理器中不存在这个内存地址所对应的数据正本, 处于这个状态的缓存条目, 其缓存行中蕴含的数据与主内存中蕴含的数据是 统一的.
    • Modified(更改过的, 记为 M): 示意相应缓存行 蕴含 相应的内存地址对应的更新后果, 并且, 因为 MESI 协定中规定任意时刻只能有一个处理器对同一个内存地址的数据做更改, 所以其它处理器不存在这个内存地址所对应的数据正本, 处于这个给状态的缓存条目, 其缓存行中蕴含的数据是与主内存中蕴含的数据是 不统一的.

    MESI 协定在这四种状态的根底上定义一组音讯用于协调各个处理器之间的读写操作, 对比 HTTP 协定, 咱们能够将 MESI 中的音讯分为: 申请音讯 – 相应音讯, 处理器在执行内存的读写操作时会向总线发送相应的音讯, 其它处理器会拦挡总线中的音讯并在肯定条件下往总线中回复相应的响应音讯.

    音讯名 音讯类型 形容
    Read 申请 告诉其它处理器、主内存以后处理器筹备读取某个数据. 该音讯中蕴含筹备读取音讯的内存地址.
    Read Response 响应 该音讯蕴含被申请读取的数据, 可能来自于主内存也可能来自于其它拦挡到读取音讯的其余处理器.
    Invalidate 申请 告诉其它处理器将各自高速缓存中的对应内存地址的缓存条目标状态置为 I, 即, 告诉这些处理器删除指定内存地址所对应的数据正本.
    Invalidate Acknowledge 响应 接管到 Invalidate 音讯的处理器必须回复该音讯, 以示意它在高速缓存中删除了对应的数据正本.
    Read Invalidate 申请 该音讯是由 Read 音讯和 Invalidate 音讯组成的复合音讯, 用于告诉其它处理器, 以后处理器筹备更新一个数据(Read-Modify-Write), 并申请器它处理器删除指定内存地址的对应的数据正本, 收到这个音讯的的处理器必须回复 Read Response 音讯和 Invalidate Acknowledge 音讯
    WriteBack 申请 该音讯蕴含须要羞辱内存的数据信息和对应的内存地址
  2. MESI 读操作流程

    假如内存地址 A 上的数据 DATA 是处理器 - 1 和处理器 - 2 可能共享的数据。

    上面探讨在处理器 - 1 上读取数据 DATA 的实现。处理器 - 1 会依据地址 A 找到对应的缓存条目,并读取该缓存条目标 Tag 和 Flag 值(缓存条目状态)。为探讨不便,这里咱们不探讨 Tag 值的匹配问题。处理器 - 1 找到的缓存条目标状态如果为 M、E 或者 S,那么该处理器能够间接从相应的缓存行中读取地址 A 所对应的数据,而无须往总线中发送任何音讯。处理器 - 1 找到的缓存条目标状态如果为 I,则阐明该处理器的高速缓存中并不蕴含 DATA 的无效正本数据,此时处理器 - 1 须要往总线发送 Read 音讯以读取地址 A 对应的数据,而其余处理器处理器 -2(或者主内存)则须要回复 Read Response 以提供相应的数据。

    处理器 - 1 接管到 Read Response 音讯时,会将其中携带的数据(蕴含数据 DATA 的数据块)存入相应的缓存行并将相应缓存条目标状态更新为 S。处理器 - 1 接管到的 Read Response 音讯可能来自主内存也可能来自其余处理器(处理器 -2)。处理器 - 2 会嗅探总线中由其余处理器发送的音讯。处理器 - 2 嗅探到 Read 音讯的时候,会从该音讯中取出待读取的内存地址,并依据该地址在其高速缓存中查找对应的缓存条目。如果处理器 - 2 找到的缓存条目标状态不为 I,则阐明该处理器的高速缓存中有待读取数据的正本,此时处理器 - 2 会结构相应的 Read Response 音讯并将相应缓存行所存储的整块数据(而不仅仅是处理器 - 1 所申请的数据 DATA)“塞入”该音讯。如果处理器 - 2 找到的相应缓存条目标状态为 M,那么处理器 - 2 可能在往总线发送 Read Response 音讯前将相应缓存行中的数据写入主内存。处理器 - 2 往总线发送 Read Response 之后,相应缓存条目标状态会被更新为 S。如果处理器 - 2 找到的高速缓存条目标状态为 I,那么处理器 - 1 所接管到的 Read Response 音讯就来自主内存。可见,在处理器 - 1 读取内存的时候,即使处理器 - 2 对相应的内存数据进行了更新且这种更新还停留在处理器 - 2 的高速缓存中而造成高速缓存与主内存中的数据不统一,在 MESI 音讯的协调下这种不统一也并不会导致处理器 - 1 读取到一个过期的旧值。

  3. MESI 写操作流程

    任何一个处理器执行内存写操作时必须领有相应数据的所有权。在执行内存写操作时,处理器 - 1 会先依据内存地址 A 找到相应的缓存条目。处理器 - 1 所找到的缓存条目标状态若为 E 或者 M,则阐明该处理器曾经领有相应数据的所有权,此时该处理器能够间接将数据写入相应的缓存行并将相应缓存条目标状态更新为 M。处理器 - 1 所找到的缓存条目标状态如果不为 E、M,则该处理器须要往总线发送 Invalidate 音讯以取得数据的所有权。其余处理器接管到 Invalidate 音讯后会将其高速缓存中相应的缓存条目状态更新为 I(相当于删除相应的正本数据)并回复 Invalidate Acknowledge 音讯。发送 Invalidate 音讯的处理器(即内存写操作的执行处理器),必须在接管到其余所有处理器所回复的所有 Invalidate Acknowledge 音讯之后再将数据更新到相应的缓存行之中。

    处理器 - 1 所找到的缓存条目标状态若为 S,则阐明处理器 - 2 上的高速缓存可能也保留了地址 A 对应的数据正本,此时处理器 - 1 须要往总线发送 Invalidate 音讯。处理器 - 1 在接管到其余所有处理器所回复的 Invalidate Acknowledge 音讯之后会将相应的缓存条目标状态更新为 E,此时处理器 - 1 取得了地址 A 上数据的所有权。接着,处理器 - 1 便能够将数据写入相应的缓存行,并将相应的缓存条目标状态更新为 M。处理器 - 1 所找到的缓存条目标状态若为 I,则示意该处理器不蕴含地址 A 对应的无效正本数据,此时处理器 - 1 须要往总线发送 Read Invalidate 音讯。处理器 - 1 在接管到 Read Response 音讯以及其余所有处理器所回复的 Invalidate Acknowledge 音讯之后,会将相应缓存条目标状态更新为 E,这示意该处理器曾经取得相应数据的所有权。接着,处理器 - 1 便能够往相应的缓存行中写入数据了并将相应缓存条目标状态更新为 M。其余处理器在接管到 Invalidate 音讯或者 Read Invalidate 音讯之后,必须依据音讯中蕴含的内存地址在该处理器的高速缓存中查找相应的高速缓存条目。若处理器 - 2 所找到的高速缓存条目标状态不为 I,那么处理器 - 2 必须将相应缓存条目标状态更新为 I,以删除相应的正本数据并给总线回复 Invalidate Acknowledge 音讯。可见,Invalidate 音讯和 Invalidate Acknowledge 音讯使得针对同一个内存地址的写操作在任意一个时刻只能由一个处理器执行,从而防止了多个处理器同时更新同一数据可能导致的数据不统一问题。

写缓冲器和有效化队列

  1. MESI 协定解决了缓存一致性问题, 然而, 当一个处理器在进行写操作时, 必须期待其它处理器将对应的数据正本删除后并回复 Invalidate Acknowledge/Read Response 音讯能力将数据写入高速缓存,为了解决性能问题, 便引入了写缓冲器和有效化队列.
  2. 写缓冲器时处理器外部一个比高速缓存容量还小的公有存储部件, 每个处理器都有本人的写缓冲器, 写缓冲器外部可能蕴含多个条目, 一个处理器不能读取另一个处理器的写缓冲器.
  3. 当处理器在进行写操作时, 如果对应的缓存条目为 I(或者 S), 处理器会先将写操作相干的数据 (蕴含数据和待操作的内存地址) 存入写缓冲器的相干条目当中, 并发送 Read Invalidate(Invalidate)音讯, 若其它所有处理器对应的条目状态全副也为 I, 那么, 就产生了所谓的 ” 写未命中 ”, 即 Read 申请会进行主内存读操作, 这样是开销比拟大的, 所以处理器在收到主内存返回的相应内存地址对应的数据, 将其写入写缓冲器后, 就不再期待其它处理器回复 Read Response /Invalidate Acknowledge 音讯而是继续执行其它指令, 等到所有处理器的 Read Response /Invalidate Acknowledge 音讯返回胜利, 那么写缓冲区再将数据写入高速缓存.
  4. 处理器在收到 Invalidate 音讯后并不删除指定缓存条目中的数据, 而是先将音讯存入到有效化队列, 而后回复 Invalidate Acknowledge 音讯, 以缩小处理器的等待时间, 而后再从有效化队列中读取音讯删除相干数据, 某些处理器并不领有有效化队列(比方 X86 处理器)

存储转发

处理器在进行读操作的时候, 因为对应内存地址的变量可能刚写完, 还没有从写缓冲器同步到高速缓存中去, 所以在解决读的时候, 会先去写缓冲器中读取是否存在相应的条目, 如果没有就去高速缓存中读取, 然而一个处理器并不能读取其它处理器的写缓冲区.

内存重排序

写缓冲器和有效化队列都可能导致内存重排序.

  1. 写缓冲器可能导致 StoreLoad 重排序

    Processor 0 Processor 1
    X=1; //S1 Y=1; //S3
    r1=Y; //L2
    r2=X; //L4

    如上表 X、Y 均为共享变量其初始值均为 0,r1、r2 为局部变量

    当 Processor 0 执行到 L2 的时候, 如果 S3 的操作的后果还处于写缓冲器之中, 那么 L2 读取到 Y 的值还是初始值 0, 同样当 Processor 1 执行 L4 的时候 S1 的操作后果也还处于写缓冲器之中, 那么 r2 读取到 X 的值也为初始值 0, 对于此时的 Processor 1 看来 S1 是没有产生的, 即 Processor 的执行程序为 L2→S1 这就是所谓的 StoreLoad 重排序.

  2. 写缓冲器可能导致 StoreStore 重排序

    Processor 0 Processor 1
    data=1; //S1
    ready=true; //S2
    while(! ready) continue; //L3
    print(data); //L4

    如上表 data 和 ready 是共享变量, 初始值为 0 和 false, 在执行之前它们在 Processor 0 处理器所对应的缓存条目状态别离为 S(或者 I)和 E, 在 Processor 1 上对应的缓存条目为 S 和 I

    1. 当执行到 S1 时,Processor 0 会先把 S1 的操作后果存到写缓冲器中, 而后向其它处理器收回 Invalidate 申请 ….
    2. ready 因为时 Processor 0 独享的数据, 所以 S2 的后果被间接存入到了高速缓存当中
    3. L3 通过缓存一致性协定胜利读取到了 ready 的新值 true
    4. L4 因为 S1 的操作后果还没有从写缓冲器同步至高速缓存, 所以读取的的 data 值还是一个旧值为 0

    就 Processor 1 感知到的程序在 Processor 0 中 ready 的值曾经扭转,data 还是初始值, 即 S2→S1, 这就是 StoreStore 重排序.

  3. 有效化队列造成 LoadLoad 重排序

    Processor 0 Processor 1
    data=1; //S1
    ready=true; //S2
    while(! ready) continue; //L3
    print(data); //L4

    如上表 data 和 ready 是共享变量, 初始值为 0 和 false, 在执行之前它们在 Processor 0 处理器所对应的缓存条目状态别离为 S 和 E, 在 Processor 1 上对应的缓存条目为 S 和 I

    1. S1 是对 data 变量做批改,Processor 0 会先将 S1 的操作后果放入写缓冲器, 而后向总线发送携有 data 内存地址信息的 Invalidate 申请,Processor 1 拦挡到这个申请后, 回复一个 Invalidate Acknowledge 音讯, 而后, 把申请放入有效化队列
    2. S2 中 ready 是 Processor 0 独占的, 所以 Processor 0 间接将 ready 批改为 true 存到高速缓存中
    3. L3 通过缓存一致性协定从 Processor 0 中同步了 ready 的值,while 条件为 false 进入 L4
    4. L4 中可能呈现这种状况, 因为 1 中的 Invalidate 申请还在 Processor 1 的有效化队列当中, 此时 L4 还能间接从其高速缓存中读取 data 的值, 然而此时 data=0, 还是初始值

    就 Processor 0 感知到 Processor 1 中的 L4 读取的 data 是一个旧值, 即执行程序为 L4→L3, 这就是 LoadLoad 重排序

可见性问题

可见性问题是由写缓冲器和有效化队列造成的, 而解决可见性问题的办法就是在共享变量的读写时退出内存屏障.

存储屏障 → 会将写缓冲器中的内容冲刷到高速缓存, 免得最新的数据不可能被其它处理器读到.

加载屏障 → 会将有效化队列中的申请执行, 免得所在处理器读到的是一个旧值.

根本内存屏障

  1. 处理器反对哪种内存重排序 (LoadLoad 重排序,LoadStore 重排序,StoreLoad 重排序,StoreStore 重排序) 就会提供可能禁止相应重排序的指令, 这种指令被称作内存屏障(LoadLoad 屏障,LoadStore 屏障,StoreLoad 屏障,StoreStore 屏障).
  2. 屏障的具体作用是禁止指令之间的重排序, 例如 LoadStore 屏障就是禁止这个屏障之前的读指令与这个屏障之后的写指令重排序.
  3. LoadLoad 屏障 → 解决 LoadLoad 重排序(上文提到过是由有效化队列造成的)

    所以 LoadLoad 屏障的实现原理就是通过清空有效化队列中的 Invalidate 音讯来删除高速缓存中的有效副原本保障不产生重排序的.

    加载屏障的实现形式

  4. StoreStore 屏障 → 解决 StoreStore 重排序

    通过对写缓冲器中的条目进行标记来实现的, 通过来判断条目标提交程序, 如果处理器在进行写操作的过程中发现写缓冲器中的条目存在标记景象, 那么即便这个写操作对应的高速缓存中的数据的条目状态为 E 或 M, 也会将写的数据存入写缓冲器而不是高速缓存. 这样就会保障屏障之前的写操作必定会在屏障之后的操作后面提交至高速缓存

  5. StoreLoad 屏障 → 很多处理器的根本屏障

    StoreLoad 屏障可能代替其它任何屏障的作用, 它的次要操作是冲刷写缓冲器缓存 + 清空有效化队列, 所以 StoreLoad 的屏障的开销也是最大的.

同步机制和内存屏障

  1. 获取屏障 (Acquire Barrier)& 开释屏障(Release Barrier) 两种屏障是由根底屏障组成的复合屏障

    获取屏障 →LoadLoad 屏障和 LoadStore 屏障组合而成, 它能阻止屏障前的任何读操作与屏障后的读写操作产生重排序.

    开释屏障→LoadStore 屏障和 StoreStore 屏障组合而成, 它能阻止屏障后的任何写操作和屏障前的读写操作产生重排序,

  2. volatile 与存储屏障

    volatile 关键字写操作的屏障应用形式

    volatile 关键字读操作的屏障应用形式

    而后理论状况在 X86 处理器(罕用的 pc 机,英特尔处理器 & amd 处理器→其新推出锐龙的 zen 架构其实也属于 X86)下只反对 StoreLoad 重排序(LoadLoad 这种重排序基本不会产生),所以在 X86 处理器中,LoadLoad 屏障、LoadStore 屏障、StoreStore 屏障都是空指令.

  3. Synchronized 相干的存储屏障: 与 volatile 雷同根本都是由几种根底屏障组成, 然而 synchronized 润饰的是代码块, 所以它不辨别读写状况, 所须要的屏障更多, 所以开销更大, 然而最终在 X86 处理器中只会存在 StoreLoad 屏障.

    虚构机会在 MonitorEnter 指令之后的临界区开始的之前的中央插入一个加载屏障保障其它线程对于共享变量的更新可能同步到线程所在处理器的高速缓存当中. 同时, 也会在 MonitorExit 指令之后插入一个存储屏障, 保障临界区的代码对共享变量的变更能及时同步.

    虚构机会在 MonitorEnter 指令之后插入一个获取屏障, 在 MonitorExit 指令之前插入一个开释屏障.

  4. final 的内存屏障规定

    final 的重排序规定会要求译编器在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 障屏。

    读 final 域的重排序规定要求编译器在读 final 域的操作后面插入一个 LoadLoad 屏障。

    因为 x86 处理器不会对写 – 写操作做重排序,所以在 x86 处理器中,写 final 域须要的 StoreStore 障屏会被省略掉。同样,因为 x86 处理器不会对存在间接依赖关系的操作做重排序,所以在 x86 处理器中,读 final 域须要的 LoadLoad 屏障也会被省略掉。也就是说在 x86 处理器中,final 域的读 写不会插入任何内存屏障!

虚拟机对内存屏障的优化

这些优化蕴含省略、合并等, 比方两个间断的 volatile 写操作, 虚拟机只会在最初一个写操作的前面加一个 StoreLoad 屏障,X86 处理器对每个 MonitorExit 的实现就带有 StoreLoad 的成果, 所以就不须要在它前面加 StoreLoad 屏障.

正文完
 0