在之前我们聊过java的内存模型,以及通过volatile、synchorinized等关键字来确保多线程下并发的原子性、内存可见性、顺序性 语义。
今天我们来聊聊,JVM为什么要这么干,背后的底层原因是什么。
在现代计算机硬件体系下,一般CPU都是多核模式,同时可以有多个线程并发工作。CPU在工作时,需要加载相关的数据,然后到CPU的相关计算单元进行计算。CPU的速度特别快,而一般数据都是在硬盘、内存里。所以如果要加载磁盘上的文件,一般会先通过相关驱动程序将磁盘上的文件读取到内存中,CPU在从内存中加载数据进行运算,但是CPU从内存加载数据相比CPU的速度还是太慢了,于是乎就有了大家经常听到的CPU缓存,或者听说过的cache line的东西。现代CPU一般都有三层缓存,c分别为L1 CACHE,L2 CACHE,L3CACHE。CPU加载数据的时候先从L1 CACHE加载数据,如果没有在从L2加载,L2没有的话,在从L3。有的人可能就会有疑问为什么不字节整一个L1 CACHE就行了,搞那么多级,其实主要还是成本,L1 CACHE的成本最高。注意这三层缓存是在CPU内部的,也就是CPU的制造厂商集成封装在CPU内部的。(说句题外话,cpu利用缓存也是运用了计算机中常说的28法则,这里就是我们经常用到的数据只会占所用数据的20%左右,以及常说的时间局部性和空间局部性原理)
在多核模式下,这时候CPU的结构如下:

可以看到,一般L1 cache分为数据和指令两个缓存,L1,L2 cache单个CPU独有,L3 CACHE多个CPU共享。
CPU cache中的数据是从内存中一块一块读取过来的,并不是每次读取一个数据只读取一个数据,而是读取一个块,在CPU cache中,这样的块称为Cache Line(缓存行),cpu cache会按照Cache Line将一个cache分为多个cache line,并编号
在内存中的每个数据都有一个地址,而CPU从主内存加载数据到CPU cache的时候,则是通过内存中的地址和和cache中缓存行编号取模,从而得到这个内存块数据应该放在缓存行的哪个位置,但是由于缓存行的个数肯定是比内存块的个数少,因此可能会出现多个内存块加载到同一个缓存行的情况。因此CPU Cache Line中除了缓存的数据外还有组标记、有效位两个信息。
CPU在读取数据的时候并不是按照Cache Line来读取的(从内存加载数据到CPU Cache中是按照Cache Line加载的),而是按需读取一个数据片段,一般为一个字,我们看下内存地址数据映射到CPU CACHE中的一个大概图示:

如上图,当我们从内存地址加载一个数据到CPU CACHE的时候,按照Cache Line大小加载这个内存地址的临近数据到CPU Cache中,并按照上述设置有效位,组标记,存储实际数据。
当CPU需要读取一个内存地址的数据的时候,首先判断该地址数据是否在CPU CACHE中,通过内存地址的这个标志位来判断,如果在对应的Cache Line中,组标记一样,那么表示这个内存地址的数据被加载到了CPU CACHE中,然后通过Offset信息从缓存行上读取需要的数据。如果组标记不一样,那么该内存地址数据没有被加载到CPU CACHE中,那么去内存装载数据到CPU CACHE中去。
tips: Linux下查看L1,L2,L3cache大小以及CPU每次从内存装载数据缓存行的大小


当我们在CPU里加上缓存之后,虽然能够提升速度,但是同时也带来了问题:
- 数据如何更新
- 多个CPU之间如何保证数据的一致性
首先对于数据更新来说,有两种策略:
- 写直达 Write through:这时候当cpu缓存中的数据发生更改时,同步写回到内存中,这种方式比较低效
- 写回 Write back:这时候数据更新只写回到cache中,不直接写到内存中,当cache中的数据因为cache容量不够需要被替换时才写回到内存中
可以明显的发现,比起写直达,写回的方式要更高效。但是在多CPU模式下随之而来的一个问题是,多个CPU之间如何保证数据的一致性?我们知道现在程序都是多线程运行的,多个CPU可能会加载同一个数据,那么当多个CPU两个或以上发生了数据更新,这时

本文介绍了JVM中volatile实现的底层原理。先阐述了现代计算机硬件体系下CPU缓存的相关知识,包括多级缓存、缓存行等。接着讲解了CPU缓存一致性的MESI协议及其存在的问题,以及后续的优化措施。还提到了流水线技术的优缺点。最后说明Java通过JMM屏蔽底层差异,volatile基于内存屏障实现内存可见性和顺序性。
最低0.47元/天 解锁文章
798

被折叠的 条评论
为什么被折叠?



