分析Jvm模型,涉及多个层面的知识,需要从以下三个层面一起来分析。
- java层面
- jvm层面
- 硬件层面
抽象的JMM模型
当一个线程修改了共享变量的值,其他线程能够看到修改的值,则具有可见性。
Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。
可见性保障方式:
- 通过 volatile 关键字保证可见性。
- 通过 内存屏障保证可见性。
UnsafeFactory.getUnsafe().storeFence() - 通过 synchronized 关键字保证可见性。
- 通过 Lock保证可见性。
- 通过 final 关键字保证可见性
实现原理分析:
- volatile
java层面:volatile只是关键字,看不出太多东西。
JVM层面:volatile实现的是内存屏障
#templateTable_x86_64.cpp volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad |Assembler::StoreStore)); #assembler_x86.hpp lock(); // lock前缀指令 #orderAccess_linux_x86.inline.hpp inline void OrderAccess::storeload() { fence(); } inline void OrderAccess::fence() { if (os::is_MP()) { // always use locked addl since mfence is sometimes expensive #ifdef AMD64 __asm__ volatile ("lock; addl 实际生效的只有StoreLoad写读屏障,0(%%rsp)" : : : "cc", "memory"); #else __asm__ volatile ("lock; addl
硬件层面:
可见,x86处理器中,都是通过Lock#前缀指令在执行期间锁住总线,使其他处理器无法通过总线访问内存。Lock前缀执行具有类似内存屏障的功能,禁止其前后的读写指令重排序。并且Lock#指令会等待它之前的所有指令完成、且所有缓冲buffer写回主存后开始执行,并根据缓存一致性协议,刷新buffer的 *** 作将导致其他处理器cache中的临时副本失效。
-
线程加锁
一般线程加锁的 *** 作,或是线程让出Cpu使用权,都会发生上下文切换。上下文切换的过程中,cpu完成切换的时间在5ms-10ms,此时会导致处理器本地cache失效,需要重新从主存中加载变量,此时可以读取到最新的变量值,以完成变量的可见性。
-
内存屏障
诸如:UnsafeFactory.getUnsafe().storeFence()调用JVM提供的内存屏障。
在JSR规范中定义了4种内存屏障(
1)LoadLoad屏障
(指令Load1;LoadLoad;Load2),在Load2及后续读取 *** 作要读取的数据被访问前, 保证Load1要读取的数据被读取完毕。
2)LoadStore屏障
指令Load1;LoadStore;Store2,在Store2及后续写入 *** 作被刷出前,保证Load1 要读取的数据被读取完毕。
3)StoreStore屏障
指令Store1;StoreStore;Store2,在Store2及后续写入 *** 作执行前,保证Store1的 写入 *** 作对其它处理器可见。
4)StoreLoad屏障
指令Store1;StoreLoad;Load2,在Load2及后续所有读取 *** 作执行前,保证 Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。由于x86只有store load可能会重排序,所以只有JSR的StoreLoad屏障对应它的mfence或lock前缀指令, 其他屏障对应空 *** 作。
硬件层提供了一系列的内存屏障memory barrier / memory fence(Intel的提法)来提供一致性的能力。拿X86平台来说,有几种主要的内存屏障:
- mfence, 是一种全能型的屏障,具备lfence和sfence的能力
- Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。 阻止屏障两边的指令重排序刷新处理器缓存/冲刷处理器缓存
内存屏障有两个能力:
- 通过 volatile 关键字保证有序性。
- 通过 内存屏障保证有序性。
对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据;对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。
Lock前缀实现了类似的能力,它先对总线和缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的数据刷新回主内存。在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。
由于程序员所写的代码,交由JVM来处理时都存在一定优化空间。为了充分压榨CPU性能,减少Cpu等待时间(机协同感),JVM编译器与CPU处理器都会执行重排序优化 *** 作。
如何保证有序性
- 通过 synchronized关键字保证有序性。
- 通过 Lock保证有序性。 保障所有单线程中会产生不同结果的排序禁止重排序
- 当处理器修改缓存行的变量时,改缓存行的状态修改为Modify(修改)
实现原理分析:
volatile有序性原理(
内存屏障(核心思想:保障volitale写完不能和其他volatile读写乱序、保障volatile读完禁止和其他读写 *** 作重排序)
volatile只能保证32位系统下的long和double的写 *** 作的原子性。一般认为volatile不具备原子性。
4、用户态与内核态的关系 5、CPU缓存架构及缓存一致性先看计算机组成
由于Cpu的缓存多级缓存架构中,各处理器均有各自的缓存(L1、L2)。处理器间如果没有一套机制来保证缓存的一致性,大家各自为战,无法实现最终处理结果的统一。
总线锁定就是使用处理器提供的一个 LOCK#信号,当其中一个处理器在总线上输出此信号时,其它处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
总线窥探(Bus Snooping)工作原理
当特定数据被多个缓存共享时,处理器修改了共享数据的值,更改必须传播到所有其他具有该数据副本的缓存中。这种更改传播可以防止系统违反缓存一致性。数据变更的通知可以通过总线窥探来完成。所有的窥探者都在监视总线上的每一个事务。如果一个修改共享缓存块的事务出现在总线上,所有的窥探者都会检查他们的缓存是否有共享块的相同副本。如果缓存中有共享块的副本,则相应的窥探者执行一个动作以确保缓存一致性。这个动作可以是刷新缓存块或使缓存块失效。它还涉及到缓存块状态的改变,这取决于缓存一致性协议(cache coherence protocol)。
MESI协议
将内存中的数据,通过缓存行(cache line)的方式,每行占用64字节。
- 当多个处理器共享变量时,该缓存行的状态修改为Sharing(共享)
- 当有处理器修改了该共享变量,其他处理器该缓存行的状态变为Invalid(无效的)
这当然也会造成很多伪共享问题(不需要将当前变量失效时,因为多变量在同一缓存行,导致需要回buffer中重新读取问题)
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)