第十一章多线程编程的硬件基础与Java内存模型

允我心安 提交于 2020-01-24 23:55:54

高速缓存内部结构示意图
高速缓存内部结构示意图
缓存条目的结构
缓存条目的结构
Data Block也被称为缓存行,它是高速缓存与主内存之间的数据交换最小单元,用于存储从主内存中读取或准备写往内存的数据。Tag则包含了与缓存行中数据相应的内存地址的部分信息(内存地址的高位部分比特)。Flag则用于表示响应缓存行的状态信息。
内存地址的解码结果包括tag、index以及offset三部分,index相当于桶号,tag相当于缓存条目的相对编号,offset是缓存行内的位置偏移。
MESI协议中一个缓存条目的Flag值有以下四种可能:Invalid(无效的,记为I)、shared(共享的,记为S),Exclusive(独占的,记为E)和Modified(更改过的,记为M)。

Processor 0读取数据S的实现
Processor 0读取数据的实现
Processor 0写数据S的实现
Processor 0写数据的实现
写缓冲器
写缓冲器是处理器内部的一个容量比高速缓存还小的私有高速存储部件。一个处理器无法读取另外一个处理器的写缓冲器中的内容。
内存写操作的执行处理器在将写操作的相关数据写入写缓冲器之后便认为该写操作已经完成,一个处理器接收到其他处理器所回复的针对同一个缓存条目的所有Invalidate Acknowledge消息的时候,该处理器会将写缓冲器中针对相应地址的写操作的结果写入相应的缓冲行中。
无效化队列
引入无效化队列后,处理器在接收到Invalidate消息之后并不会删除消息中指定地址对应的副本数据,而是将消息存入无效化队列之后就回复Invalidate Acknowledge消息,从而减少了写操作执行处理器所需的等待时间。
存储转发
处理器在执行读操作的时候会根据相应的内存地址查询写缓冲器。如果写缓冲器存在相应的条目,那么该条目所代表的写操作的结果数据就会直接作为该读操作的结果返回;否则处理器才从高速缓存中去读取数据。
重排序
写缓冲器可能导致StoreLoad重排序,如一个值的修改值在Processor 1写缓冲器中,那么其他处理器读到的还是Processor 1高速缓存器中的旧值
写缓冲器可能导致StoreStore重排序,同样也是因为新值在写缓冲器中,读取到的是旧值。
无效化队列可能导致LoadLoad重排序,原因是Invalidate消息还在无效化队列中,数据的值未被修改。
可见性
我们必须将写缓冲器中的内容写入其所在的处理器的高速缓存之中,即冲刷缓冲器,这需要内存屏障中的存储屏障;同时我们还需要处理无效化队列,这需要加载屏障。
写线程的执行处理器所执行的存储屏障保障了该线程对共享变量所作的更新对读线程来说是同步的;读线程的执行处理器所执行的加载屏障将写线程对共享变量所作的更新同步到该处理器的高速缓存中。
基本内存屏障
基本内存屏障是对一类指令的称呼,这类指令的作用是禁止该指令左侧的任何X操作与该指令右侧的任何Y操作之间进行重排序。比如StoreLoad屏障能够禁止其左侧的任何写操作与其右侧的任何读操作之间进行重排序。
LoadLoad屏障是通过清空无效化队列来实现禁止LoadLoad重排序的。
StoreStore屏障可以通过对写缓冲器中的条目进行标记来实现禁止StoreStore重排序。
StoreLoad屏障会清空无效化队列,并将写缓冲器中的条目冲刷(写入)高速缓存。

Java同步机制与内存屏障

获取屏障相当于LoadLoad屏障和LoadStore屏障的组合,它能够禁止该屏障之前的任何读操作与该屏障之后的任何读、写操作之间进行重排序。释放屏障相当于LoadStore屏障和StoreStore屏障的组合,它能够禁止该屏障之前的任何读、写操作与该屏障之后的任何写操作之间之间重排序。
volatile
有序性
Java虚拟机(JIT编译器)在volatile变量写操作之前插入的释放屏障使得该屏障之前的任何读、写操作都先于这个volatile变量写操作被提交,而Java虚拟机(JIT编译器)在volatile变量读操作之后插入的获取屏障使得这个volatile变量读操作先于该屏障之后的任何读、写操作被提交。
可见性
Java虚拟机(JIT编译器)在volatile变量写操作之后插入一个StoreLoad屏障。该屏障可以充当存储屏障,StoreLoad屏障通过清空其执行处理器的写缓冲器使得该屏障前的所有写操作的结果得以达到高速缓存,从而使这些更新对其他处理器而言是可同步的;充当加载屏障,以消除存储转发的副作用。
Java虚拟机(JIT编译器)在volatile变量读操作前插入一个加载屏障相当于LoadLoad屏障,它通过清空无效化队列来使得其后的读操作有机会读取到其他处理器的共享变量所做的更新。
可见,volatile对可见性的保障是通过写线程、读线程配对使用存储屏障和加载屏障实现的。
synchronized
有序性
Java虚拟机(JIT编译器)在monitorenter(用于申请锁的字节码指令)对应的指令后临界区开始前的地方插入一个获取屏障。Java虚拟机会在临界区结束后monitorexit(用于释放锁的字节码指令)对应的指令前的地方插入一个释放屏障。
可见性
Java虚拟机会在monitorexit对应的指令(相当于写操作)之后插入一个StoreLoad屏障。该屏障充当了存储屏障,从而确保锁的持有线程在释放锁之前所执行的所有操作的结果能够到达高速缓存,并消除了存储转发的副作用。
优化
内存屏障部分禁止重排序的代价就是它会阻止编译器(JIT编译器)、处理器做一些性能优化。另外一种代价就是其实现往往涉及冲刷写缓冲器和清空无效化队列,而这两个动作可能是比较耗时的。
优化包括省略、合并等。
final
Java虚拟机会在子操作final字段初始化之后插入一个StoreStore屏障以禁止子操作final字段初始化以及该操作前的所有写操作和对象发布之间的重排序。
包含final字段的对象引用对外可见的时候该对象的非final字段仍然可能是未初始化完毕的。

Java内存模型

对引用类型以及几乎所有基本数据类型的共享变量进行的读、写操作,Java内存模型都保证它们具有原子性,而对long/double型的共享变量进行的读、写操作是否具有原子性而取决于具体的Java虚拟机实现。
happen(s)-before关系
假设动作A和动作B之间存在happens-before关系,称之为A happens-before B,那么Java内存模型保证A的操作结果对B可见,即A的操作结果会在B被执行前提交
程序顺序规则:一个线程中的每一个动作都happens-before该线程中程序顺序上排在该动作之后的每一个动作。
内部锁规则:内部锁的释放happens-before后续每一个对该锁的申请。尽管锁对排他性的保障仅限于临界区内的代码,但是锁对可见性和有序性的保障却可以扩展到临界区之前。
volatile变量规则:对一个volatile变量的写操作happens-before后续每一个针对该变量的读操作。
线程启动规则:调用一个线程的start方法happens-before被启动的这个线程中的任何一个动作。
线程终止规则:一个线程中的任何一个动作都happens-before该线程的join方法的执行线程在join方法返回之后所执行的任意一个动作。
对象的安全发布
对象安全发布的实质——不仅仅使一个对象的引用对其他线程可见,还要保障该对象的引用对其他线程可见前,发布线程对该对象所执行的操作对其他线程来说是可见且有序的。

小结

  1. 高速缓存是一个存取速率远比主内存大而容量远比主内存小的存储部件,其引入弥补了处理器与主内存处理能力之间的鸿沟。高速缓存相当于一个由硬件实现的散列表,其键为内存地址,其值为从内存读取或者准备写入内存的数据。高速缓存中的每一个桶可包含若干个缓存条目。缓存条目中的Tag部分包含了内存地址的高位部分比特;Flag部分指示了缓存条目的有效性;缓存行用于存储从内存读取或者准备写入内存的数据,其容量在16~256字节之间不等,一个缓存行可用于存储多个变量。缓存命中意味着待读取或者写入内存的数据在高速缓存中存在相应的副本,这可以提高内存访问效率。缓存未命中包括读未命中和写未命中,它不利于性能,但是由于高速缓存容量的限制又往往是不可避免的。Linux内核工具perf可用来查看缓存未命中情况。现代处理器多采用多级高速缓存,典型的高速缓存层级包括L1 Cache、L2 Cache和L3 Cache.
  2. 缓存一致性协议保障了多个处理器上高速缓存中的数据副本的数据一致性,避免了一个处理器读取到共享变量的旧值以及避免一个处理器对共享变量所作的更新丢失。MESI协议是一个广为使用的缓存一致性协议,该协议下的缓存条目Flag可能值包括:M/E/S/I。内存读/写操作是通过处理器发送与接收相关消息并更新缓存条目的Flag实现的。这些消息包括:Read/Read Response、Invalidate/Invalidate Acknowledge、Read Invalidate、Writeback。
  3. 写缓冲器与无效化队列的引入弥补了MESI协议的性能缺点。
  4. 写缓冲器是处理器内部的一个容量比高速缓存还小的私有高速存储部件。其引入使得内存写操作的执行处理器无序等待其他处理器回复Invalidate Acknowledge/Read Response消息便可以执行其他指令,从而减小内存写操作的延迟。写缓冲器能导致写线程对共享变量所做的更新无法被其他处理器同步过去。存储转发技术使得一个处理器可以直接从写缓冲器中读取该处理器先前执行的写操作的结果,但是它也可能导致可见性问题。另外,写缓冲器还会导致StoreLoad重排序和StoreStore重排序。
  5. 无效化队列的引入使得处理器在接收到Invalidate消息之后可以立即回复Invalidate Acknowledge消息,这减少了发送Invalidate消息的处理器的等待时间。无效化队列可能使写线程对共享变量所做的共享无法反映到读线程执行处理器的高速缓存中,即导致可见性问题。无效化队列可以导致LoadLoad重排序。
  6. 从硬件的角度来看,可见性的保障是通过写线程和读线程配对使用存储屏障和加载屏障实现的。存储屏障能够冲刷写缓冲器使得写线程对共享变量所作的更新能够被其他处理器同步,加载屏障能够清空无效化队列,使得写线程对共享变量所做的更新能够反映在读线程执行处理器的高速缓存之中。
  7. 获取屏障相当于LoadLoad屏障和LoadStore屏障的组合,释放屏障相当于StoreStore屏障和StoreLoad屏障的组合(有序)。LoadLoad屏障相当于加载屏障;而StoreLoad屏障是“全能型”屏障,它既可以充当存储屏障,也可以充当加载屏障。(可见)
  8. Java虚拟机(JIT编译器)为了确保final关键字的语义,会在final字段初始化与构造器返回之前插入一个StoreStore屏障,这使得final字段初始化操作无法被重排到构造器之外,从而确保了构造器返回之后相应对象的final字段总是初始化完毕的。有序性的保障是通过写线程与读线程配对执行释放屏障和获取屏障实现的,同样这些屏障也是Java虚拟机(JIT编译器)替我们的应用程序插入的。Java虚拟机(JIT编译器)会在volatile变量写操作之后插入一个StoreLoad屏障,该屏障不仅充当了存储屏障以冲刷写缓冲器,还充当了加载屏障以清空无效化队列从而清除了存储转发技术的副作用。Java虚拟机(JIT编译器)会在volatile变量读操作前插入一个LoadLoad屏障,该屏障充当了加载屏障,用于清空无效化队列。
  9. Java内存模型从“什么”(What)的角度来回答线程安全有关问题,JSR 133对Java内存模型进行了增强和修复。Java内存模型规定,long/double型变量以外的任何变量的读/写操作具有原子性;volatile变量修饰的long/double型变量的读/写操作也具有原子性。long/double型普通变量的读/写操作的原子性取决于具体的Java虚拟机。happens-before从可见性的角度对有序性进行描述。happens-before关系具有传递性和积累效果。Java内存模型定义的happens-before规则包括:程序顺序规则、内部锁规则、volatile规则、线程启动规则和线程终止规则。Java标准库本身也定义了一些happens-before规则。从语言的层面来看,这些规则是通过使用Java的同步机制实现的;从底层的角度来看,这些规则是由Java虚拟机、编译器以及处理器一同协作来落实的,内存屏障则是Java虚拟机、编译器和处理器之间的“沟通”纽带。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!