聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障
来源:互联网 发布:js给span标签赋值 编辑:程序博客网 时间:2024/05/07 13:30
原文 http://blog.csdn.net/iter_zc/article/details/42006811
在聊聊高并发(三十三)从一致性(Consistency)的角度理解Java内存模型 我们说了硬件层提供了满足某些一致性需求的能力,Java内存模型利用了硬件层提供的能力指定了一系列的语法和规则,让Java开发者可以隔绝这种底层的实现专注于并发逻辑的开发。这篇我们来看看硬件层是如何提供这些实现一致性需求的能力的。
硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供一致性的能力。拿X86平台来说,有几种主要的内存屏障
1. lfence,是一种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前缀实现了类似的能力,
1. 它先对总线/缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的脏数据全部刷新回主内存。
2. 在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。Lock后的写操作会让其他CPU相关的cache line失效,从而从新从内存加载最新的数据。这个是通过缓存一致性协议做的。
再引用一下这篇文章中关于Lock前缀的更多的一些描述 多处理器多线程使用_asm lock指令能保证互斥吗?
“从 P6 处理器开始,如果指令访问的内存区域已经存在于处理器的内部缓存中,则“lock” 前缀并不将引线 LOCK 的电位拉低,而是锁住本处理器的内部缓存,然后依靠缓存一致性协议保证操作的原子性。
IA32 CPU调用有lock前缀的指令,或者如xchg这样的指令,会导致其它的CPU也触发一定的动作来同步自己的Cache。
CPU的#lock引脚链接到北桥芯片(North Bridge)的#lock引脚,当带lock前缀的执行执行时,北桥芯片会拉起#lock
电平,从而锁住总线,直到该指令执行完毕再放开。 而总线加锁会自动invalidate所有CPU对 _该指令涉及的内存_
的Cache,因此barrier就能保证所有CPU的Cache一致性。
lock前缀(或cpuid、xchg等指令)使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。
IA32在每个CPU内部实现了Snoopying(BUS-Watching)技术,监视着总线上是否发生了写内存操作(由某个CPU或DMA控
制器发出的),只要发生了,就invalidate相关的Cache line。 因此,只要lock前缀导致本CPU写内存,就必将导致
所有CPU去invalidate其相关的Cache line。 ”
内存屏障的概念很好理解,不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。
看一下volatile的实现
有些材料里说Java实现volatile的时候使用了类似mfence等内存屏障,但是我经过测试发现在X86平台上volatile是用Lock前缀来实现的,测试的是JDK6和7。
下面来看一下volatile生成的汇编码
写volatile的时候生成汇编码是 lock addl $0x0, (%rsp), 在写操作之前使用了lock前缀,锁住了总线和对应的地址,这样其他的写和读都要等待锁的释放。当写完成后,释放锁,把缓存刷新到主内存。
1. 读volatile就很好理解了,不需要额外的汇编指令,CPU发现对应地址的缓存被锁了,等待锁的释放,缓存一致性协议会保证它读到最新的值。
2. 只需要对写volatile的使用用lock对总线加锁就行了,这样其他的读、写操作等待总线释放才能继续读。Lock会让其他CPU的缓存invalide,从内存重新加载数据。
再看一下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的方式修改对象头的锁标志位为”释放“状态,写操作都立刻写回主内存。JVM会进一步对synchronized时CAS失败的那些线程进行阻塞操作,这部分的逻辑没有体现在lock cmpxchg指令上,我猜想是通过某种信号量来实现的。lock cmpxchg指令前者保证了可见性和防止重排序,后者保证了操作的原子性。
参考资料:
Memory Barriers/Fences
LOCK vs MFENCE
Memory barrier
多处理器多线程使用_asm lock指令能保证互斥吗?
- 聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障
- 聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障
- 聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障
- 聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障
- 聊聊高并发(三十六)Java内存模型那些事(四)理解Happens-before规则
- 聊聊高并发(三十四)Java内存模型那些事(二)理解CPU高速缓存的工作原理
- 聊聊高并发(三十三)Java内存模型那些事(一)从一致性(Consistency)的角度理解Java内存模型
- 聊聊高并发(四)Java对象的表示模型和运行时内存表示
- 聊聊高并发(四)Java对象的表示模型和运行时内存表示
- Java内存模型Cookbook(二)内存屏障
- java内存模型 内存屏障
- 深入理解java内存模型(三)
- JAVA高并发学习笔记(三) JMM(Java内存模型)
- [高并发Java 三] Java内存模型和线程安全
- 【高并发Java三】Java内存模型和线程安全
- 高并发Java 三 Java内存模型和线程安全
- 高并发Java 三 Java内存模型和线程安全
- java高并发之三---JVM内存模型
- Java多线程 -- JUC包源码分析4 -- 各种锁与无锁
- indexPathForSelectedRow returning nil
- SSM框架——详细整合教程(Spring+SpringMVC+MyBatis)
- C# 三种动态调用WebService的方法,测试通过
- tnsnames.ora的目录
- 聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障
- Python 是如何找模块的呢?
- 上传文件-java.lang.NoSuchFieldException: resourceEntries
- java.lang.IllegalStateException: Optional int parameter 'pageSize' is present but cannot be translat
- C语言选择排序
- HTML中     &thinsp等空白空格的区别
- Wide character in print at check_cert.pl line 18.
- 关于感知学习模型机中空间任一点到超平面的距离公式的推导过程
- Java配置问题一:has value '1.8', but '1.7' is required.