一、CPU和内存的交互
在计算机中,CPU和内存的交互最为频繁,相比内存,磁盘读写太慢,内存相当于高速的缓冲区。
在多核CPU中,每个处理器都有各自的高速缓存(L1,L2,L3),而主内存确只有一个 。
越靠近CPU的缓存越快也越小。所以L1缓存(一级缓存)很小但很快,并且紧靠着在使用它的CPU内核。L2大一些,也慢一些,并且仍然只能被一个单独的 CPU 核使用。L3在现代多核机器中更普遍,仍然更大,更慢,并且被单个插槽上的所有 CPU 核共享。
当CPU执行运算的时候,它先去L1查找所需的数据,再去L2,然后是L3,最后如果这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。
| 从CPU到 | 大约需要的CPU 周期 | 大约需要的时间 |
|---|---|---|
| 主存 | 约60-80纳秒 | |
| QPI总线传输(between sockets, not drawn) | 约20ns | |
| L3 cache | 约40-45 cycles | 约15ns |
| L2 cache | 约10 cycles | 约3ns |
| L1 cache | 约3-4 cycles | 约1ns |
| 寄存器 | 1 cycle |
二、缓存行
数据在缓存中不是以独立的项来存储的,缓存是由缓存行组成的,通常是64字节,并且它有效地引用主内存中的一块地址。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。
非常奇妙的是如果你访问一个long数组,当数组中的一个值被加载到缓存中,它会额外加载另外7个。因此你能非常快地遍历这个数组。
因此如果数据结构中的项在内存中不是彼此相邻的(链表),你将得不到免费缓存加载所带来的优势。并且在这些数据结构中的每一个项都可能会出现缓存未命中。
伪共享问题 (缓存行对齐提高效率)
但这种加载有一个弊端。设想long类型的数据不是数组的一部分,它只是一个单独的变量。让我们称它为head。然后再设想有另一个变量tail紧挨着它。当加载head到缓存的时候同时也加载了tail。
tail正在被你的生产者写入,而head正在被你的消费者写入。这两个变量实际上并不是密切相关的,而事实上却要被两个不同内核中运行的线程所使用。
设想消费者更新了head的值。缓存中的值和内存中的值都被更新了,而其他所有存储head的缓存行都会都会失效。请记住我们必须以整个缓存行作为单位来处理,不能只把head标记为无效。
现在如果一些正在其他内核中运行的进程只是想读tail的值,整个缓存行需要从主内存重新读取。那么一个和你的消费者无关的线程读一个和head无关的值,它被缓存未命中给拖慢了。
硬件层数据一致性
MESI(Modified Exclusive Shared Or Invalid)–CPU缓存一致性协议, 是一种广泛使用的支持写回策略的缓存一致性协议。
MESI协议中的状态
CPU中每个缓存行(caceh line)使用4种状态进行标记,CPU在每个cache line 额外两位标记四种状态(使用额外的两位(bit)表示):
M: 被修改(Modified)
该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。 当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。
E: 独享的(Exclusive)
该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。
S:共享的(Shared)
该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。
I: 无效的(Invalid)
该缓存是无效的(可能有其它CPU修改了该缓存行)。
三、乱序问题
CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系。
内存屏障(Memory Barrier) 保证特定情况下不乱序
CPU执行指令可能是无序的,内存屏障有两个比较重要的作用
- 阻止屏障两侧指令重排序
- 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
硬件内存屏障 X86
- sfence: store| 在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
- lfence:load | 在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
- mfence:modify/mix | 在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
- 原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序
JVM级别如何规范(JSR133)
LoadLoad屏障:
在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。StoreStore屏障:
在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。LoadStore屏障:
在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。StoreLoad屏障:
在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
volatile的实现细节
字节码层面
- ACC_VOLATILE
JVM层面, Volatile内存区的读写都加屏障
- StoreStoreBarrier
- volatile 写操作
- StoreLoadBarrier
- LoadLoadBarrier
- volatile 读操作
- LoadStoreBarrier
synchronized实现细节
字节码层面
- ACC_SYNCHRONIZED
- monitorenter monitorexit
JVM层面
- C C++ 调用了操作系统提供的同步机制