CPU 缓存一致性协议 MESI
CPU 在摩尔定律的指导下以每 18 个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及 CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而 CPU 的高度运算需要高速的数据。为了解决这个问题,CPU 厂商在 CPU 中内置了少量的高速缓存以解决 IO 速度和 CPU 运算速度之间的不匹配问题。在 CPU 访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。
时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。
比如循环、递归、方法的反复调用等。
空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。
比如顺序执行的代码、连续创建的两个对象、数组等
带有高速缓存的 CPU 执行计算的流程
程序以及数据被加载到主内存
指令和数据被加载到 CPU 的高速缓存
CPU 执行指令,把结果写到高速缓存
高速缓存中的数据写回主内存
多核 CPU 多级缓存一致性协议 MESI
MESI 协议缓存状态
缓存行(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 状态的缓存不需要使用总线事务。
cpu 多级缓存 – 乱序执行优化
处理器为提高运算速度儿做出违背代码原有顺序的优化。
在单核处理器时代处理器的乱序执行优化不会影响执行结果。在多核处理中,某个核心执行写入操作时,将某个标志当做写入完成,进行重排优化,可能会先执行标志指令导致其他核心以为改核心已经执行完成写入操作。从而拿到错误的值。
java 内存模型(java memory model,JMM)
堆 heap
堆是运行时确定的内存,由 java GC 来维护大小,优点是可以动态的确定大小,缺点是运行时动态确定内存所以速度相对栈小一点。对象存放在堆上。静态变量跟随类一起存放在堆上。
栈 stack
栈内存的速度相对堆内存更快,仅次于寄存器,缺点是大小必须是编译期确定的。缺乏一定的灵活性,存放一些基本的数据变量(int double。。。)java 内存要求本地变量(Local Variable),调用栈必须存放在线程栈(Thead Stack)中。
本地变量可能存放的是对象的引用。当两个线程同时引用一个对象时,那么这两个线程的本地引用存放的是这个对象的私有拷贝。
硬件内存模型如图硬件内存模型和 java 内存模型的对应模型如图:
java 内存抽象模型结构
看图,本地内存:本地内存是 java 抽象的概念,涵盖了缓存,写缓存区,寄存器,其他硬件和编译器优化。本地内存储存了共享变量的副本,从硬件的角度上讲主内存就是硬件内存,但是为了获取更好的速度,java 可能会将数据存储在寄存器或者高速缓存区。如果线程要通信必须要经过主内存,流程是先在主内存中获取共享变量,存储在本地内存中经由进程计算,然后刷新至主内存,再经由其他线程访问。
java 内存模型 - 同步操作与规则
lock 和 Unlack:作用在主内存上只有在 Unlock 的情况下内存才可以被其他线程锁定。
Read:作用在主内存上,把主内存中的变量输送在工作内存中。
Load:作用工作内存中,把主内存中的值放入到工作内存副本中。
use:作用于工作内存,把数据给执行引擎。每当执行器需要使用到变量时或者执行字节码指令时会执行这个操作。
assign:赋值,在执行赋值操作时执行,将执行引擎中的值赋值给工作内存。
store:存储,把工作内存中的值传递到主内存中。
write:写入,将工作内存中的值写入到主内存中。
下面介绍一下规则,规则是用来限制每一步是如何操作的。
不允许 read 和 load、store 和 write 单一出现,因为他们是一个连贯的操作。而且必须是按顺序执行的。load 必须是 read 之后,write 必须是 store 之后,但是不一定是连续操作,在他们之间可以插入其他的指令。
不允许线程丢弃 assign 操作,也就是说执行完了之后必须放入工作内存中。
不允许线程不经过 Assign 操作直接把数据给主内存。
一个新的变量只能在主内存中诞生。
一个变量只允许一个线程对其 lack 操作,但是可以被一个线程 lack 多次,lack 多次之后只有执行相同次数的 unlack 才能被解锁。
如果一个变量执行了 lack 操作之后将会清楚工作内存中该变量的值。执行引擎在使用变量时需要重新执行 read-load-use 等操作。
如果没有执行一个 lack 操作的变量不能执行 unlack 操作。或者被其他线程执行了 lack 操作的线程也不能被改线程执行 unlack。
多线程并发的优势和缺点