分析Jvm模型,涉及多个层面的知识,需要从以下三个层面一起来分析。
当一个线程修改了共享变量的值,其他线程能够看到修改的值,则具有可见性。
Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。
可见性保障方式:
实现原理分析:
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 $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
硬件层面:
可见,x86处理器中,都是通过Lock#前缀指令在执行期间锁住总线,使其他处理器无法通过总线访问内存。Lock前缀执行具有类似内存屏障的功能,禁止其前后的读写指令重排序。并且Lock#指令会等待它之前的所有指令完成、且所有缓冲buffer写回主存后开始执行,并根据缓存一致性协议,刷新buffer的操作将导致其他处理器cache中的临时副本失效。
线程加锁
一般线程加锁的操作,或是线程让出Cpu使用权,都会发生上下文切换。上下文切换的过程中,cpu完成切换的时间在5ms-10ms,此时会导致处理器本地cache失效,需要重新从主存中加载变量,此时可以读取到最新的变量值,以完成变量的可见性。
内存屏障
诸如:UnsafeFactory.getUnsafe().storeFence()调用JVM提供的内存屏障。
在JSR规范中定义了4种内存屏障(实际生效的只有StoreLoad写读屏障)
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平台来说,有几种主要的内存屏障:
内存屏障有两个能力:
对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据;对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。
Lock前缀实现了类似的能力,它先对总线和缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的数据刷新回主内存。在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。
由于程序员所写的代码,交由JVM来处理时都存在一定优化空间。为了充分压榨CPU性能,减少Cpu等待时间(机协同感),JVM编译器与CPU处理器都会执行重排序优化操作。
如何保证有序性
实现原理分析:
volatile有序性原理(保障所有单线程中会产生不同结果的排序禁止重排序)
内存屏障(核心思想:保障volitale写完不能和其他volatile读写乱序、保障volatile读完禁止和其他读写操作重排序)
volatile只能保证32位系统下的long和double的写操作的原子性。一般认为volatile不具备原子性。
由于Cpu的缓存多级缓存架构中,各处理器均有各自的缓存(L1、L2)。处理器间如果没有一套机制来保证缓存的一致性,大家各自为战,无法实现最终处理结果的统一。
总线锁定就是使用处理器提供的一个 LOCK#信号,当其中一个处理器在总线上输出此信号时,其它处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
工作原理
当特定数据被多个缓存共享时,处理器修改了共享数据的值,更改必须传播到所有其他具有该数据副本的缓存中。这种更改传播可以防止系统违反缓存一致性。数据变更的通知可以通过总线窥探来完成。所有的窥探者都在监视总线上的每一个事务。如果一个修改共享缓存块的事务出现在总线上,所有的窥探者都会检查他们的缓存是否有共享块的相同副本。如果缓存中有共享块的副本,则相应的窥探者执行一个动作以确保缓存一致性。这个动作可以是刷新缓存块或使缓存块失效。它还涉及到缓存块状态的改变,这取决于缓存一致性协议(cache coherence protocol)。
MESI协议
将内存中的数据,通过缓存行(cache line)的方式,每行占用64字节。