在聊聊高并发(三十三)从一致性(Consistency)的角度理解Java内存模型 我们说了硬件层提供了满足某些一致性需求的能力,Java内存模型利用了硬件层提供的能力指定了一系列的语法和规则,让Java开发者可以隔绝这种底层的实现专注于并发逻辑的开发。这篇我们来看看硬件层是如何提供这些实现一致性需求的能力的。
硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供一致性的能力。拿X86平台来说,有几种主要的内存屏障
1. ifence,是一种Load Barrier 读屏障
2. sfence, 是一种Store Barrier 写屏障
3. mfence, 是一种全能型的屏障,具备ifence和sfence的能力
4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
内存屏障有两个能力:
1. 阻止屏障两边的指令重排序
2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效
对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据
对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存
Lock前缀实现了类似的能力,它先对总线和缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的脏数据全部刷新回主内存。在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。
内存屏障的概念很好理解,不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。
有些材料里说Java实现volatile的时候使用了类似mfence等内存屏障,但是我经过测试发现在X86平台上volatile是用Lock前缀来实现的,测试的是JDK6和7。
下面来看一下volatile生成的汇编码
写volatile的时候生成汇编码是 lock addl $0x0, (%rsp), 在写操作之前使用了lock前缀,锁住了总线和对应的地址,这样其他的写和读都要等待锁的释放。当写完成后,释放锁,把缓存刷新到主内存。
读volatile就很好理解了,CPU发现对应地址的缓存被锁了,等待锁的释放,缓存一致性协议会保证它读到最新的值
再看一下synchronized的实现。synchronized块生成JVM指令是monitorenter, monitorexit,最后生成的汇编指令是
lock cmpxchg %r15, 0x16(%r10) 和 lock cmpxchg %r10, (%r11)
cmpxchg是CAS的汇编指令,这里的含义是先用lock指令对总线和缓存上锁,然后用cmpxchg CAS操作设置对象头中的synchronized标志位。CAS完成后释放锁,把缓存刷新到主内存。
所以synchronized的底层操作含义是先对对象头的锁标志位用lock cmpxchg的方式设置成“锁住“状态,释放锁时,在用lock cmpxchg的方式修改对象头的锁标志位为”释放“状态,写操作都立刻写回主内存
参考资料: