CPU读取一个数,先去L0,没有再去L1一层一层得来,找到了再一层一层得加载到L0.,L1、L2是在CPU的内部的
cache line
读取缓存会以缓存行为基本单位,目前是64bytes。会出现一个伪共享问题,一个cpu改x,一个cpu改y,x、y处于同一个缓存行,一修改之后会通知其他cpu更新,再次读取此缓存行。解决方法:变量前后各存储64bytes的数据,只是用来占据cacheline,保证想要操作的数据只被一个cpu锁定,避免互相影响。实际使用的开源框架:disruptor 前后各有7个long 填充缓存行凑够64 避免伪共享
由于L1、L2是在不同的CPU内部的,所以在对同一个数据进行修改时会导致数据的不一致,硬件层是如和解决这个不一致问题的?
老CPU会使用总线锁,新CPU会使用各种各样的一致性协议,像inter使用的就是MESI一致性协议+总线锁
总线锁相当于在L2和L3中间加了一个锁,这个CPU访问这个数据时其他的访问不了,但同时其他CPU访问其他数据也访问不了,效率比较低
MESI一致性协议
缓存锁的实现之一,有些无法被缓存的数据或者跨越多个缓存行的数据依然必须使用总线锁
CPU每个cache line标记四种状态:
Modified:修改过就标记为modidfied,在其他CPU那里就标记为invalid了
Exclusive:自己独享
Shared:我读的时候别人也在读就标记为shared
Invalid:我读的时候已经被改过了
CPU乱序执行指令
对于现代cpu而言,性能瓶颈则是对于内存的访问。cpu的速度往往都比主存的高至少两个数量级,为了效率如果CPU访问的数据在缓存没找到,会去主存去找,在去找的这段时间会继续执行不依赖的指令
当cpu执行存储指令时,它会首先试图将数据写到离cpu最近的L1_cache, 如果此时cpu出现L1未命中,则会访问下一级缓存。速度上L1_cache基本能和cpu持平,其他的均明显低于cpu,L2_cache的速度大约比cpu慢20-30倍,而且还存在L2_cache不命中的情况,又需要更多的周期去主存读取。其实在L1_cache未命中以后,cpu就会使用一个另外的缓冲区,叫做合并写存储缓冲区。这一技术称为合并写入技术。在请求L2_cache缓存行的所有权尚未完成时,cpu会把待写入的数据写入到合并写存储缓冲区,该缓冲区大小和一个cache line大小,一般都是64字节。这个缓冲区允许cpu在写入或者读取该缓冲区数据的同时继续执行其他指令,这就缓解了cpu写数据时cache miss时的性能影响。
当后续的写操作需要修改相同的缓存行时,这些缓冲区变得非常有趣。在将后续的写操作提交到L2缓存之前,可以进行缓冲区写合并。 这些64字节的缓冲区维护了一个64位的字段,每更新一个字节就会设置对应的位,来表示将缓冲区交换到外部缓存时哪些数据是有效的。当然,如果程序读取已被写入到该缓冲区的某些数据,那么在读取缓存数据之前会先去读取本缓冲区的。
经过上述步骤后,缓冲区的数据还是会在某个延时的时刻更新到外部的缓存(L2_cache).如果我们能在缓冲区传输到缓存之前将其尽可能填满,这样的效果就会提高各级传输总线的效率,以提高程序性能。
在实际使用缓冲区的时候,实际上在使用时一次写入正好占满缓冲区这样效率最高,如果一次写入太多,缓冲区放不下,CPU就要暂停等待CPU缓冲区执行完再继续放入,比较浪费时间。所以一次写入刚好放满缓冲区最好。
保障有序性
硬件内存屏障X86
sfence: store| 在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
lfence:load | 在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence:modify/mix | 在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
intel lock汇编指令
原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序。
JVM级别保证有序性
- LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
voltile实现细节
- 字节码层面:变量前加ACC_VOLATILE
- JVM层面:volatile内存区的读写都加屏障
StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作
LoadStoreBar
- OS和硬件层面:hsdis - HotSpot Dis Assembler windows lock 指令实现 | MESI实现
synchronized实现细节
- 字节码层面:ACC_SYNCHRONIZED monitorenter(监视器进入) monitorexit(监视器退出)
在monitorenter后面会有两个monitorexit,一个是在执行完同步代码块之后产生的,一个是为了在执行同步代码块的时候如果出现异常会自动退出
- JVM层面:C C++ 调用了操作系统提供的同步机制
- OS和硬件层面 X86 : lock cmpxchg / xxx