JMM
硬件层数据一致
CPU 怎么保持可见呢?
- 最原始的 CPU 直接上总线锁
- 新版 CPU 采取 MESI Cache 一致性协议
因特尔:MESI 协议
Modified : 数据被修改
Exclusive : 独享的一个
Shared : 我读的时候,别人也在读
Invalid : 被别的 CPU 更改过,无效
这也是缓存锁的实现之一,有些无法被缓存的数据 或者 跨越多个缓存行的数据 依然必须要使用总线锁
现代 CPU 的数据一致性实现 = 缓存锁 + 总线锁
CPU的乱序执行
CPU 为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(慢100倍)),去同时执行另一条命令,前提是:两条指令没有任何关系
合并写【4个字节】:写操作也可以进行合并
比如 6 个字节,如果一次性写入时间 大概是 合并写时间的 2 倍
为什么呢?
【CPU 高速缓存是 4 个字节】
这是因为 我们写入的字节只有 4 位,如果按照一次性写入 6 位的话,需要先写入 4 位,然后进行等待别的 CPU 凑成 4 位进行写入
如果我们直接合并写,一次写 3 个,就不存在等待的问题
乱序执行的证明:
两个线程:
a = 0; b = 0; x = 0; y = 0; 一个线程 跑 a = 1, y = b 一个线程 跑 b = 1, x = a 看看能不能出现 y = 0 && x = 0
如何保证有序性/不乱序?
X86 因特尔 CPU 内存屏障
- sfence : 在 Store fence 指令前的写操作必须在 sfence 指令的写操作前完成
- ifence : 在 load fence 指令前的读操作必须在 Ifence 指令的读操作前完成
- mfence : 在 Mix fence 指令前的读写操作必须在 Mfence 指令的读写操作前完成
原子指令 : lock,执行时会锁住内存子系统来确保执行顺序
Volatile的实现细节
字节码
- ACC_VOLATILE : 加了一个 falg,读到这里会加 屏障
JVM
Volatile 内存区的读写,都加屏障
OS和硬件层面
hadis - HotSpot Dis Assembler
windows lock 指令实现
- 核0读取了一个字节,根据局部性原理,它相邻的字节同样被被读入核0的缓存
- 核3做了上面同样的工作,这样核0与核3的缓存拥有同样的数据
- 核0修改了那个字节,被修改后,那个字节被写回核0的缓存,但是该信息并没有写回主存
- 核3访问该字节,由于核0并未将数据写回主存,数据不同步
- 于是,在上面的情况下,核3发现自己的缓存中数据已无效,核0将立即把自己的数据写回主存,然后核3重新读取该数据。CPU制造商制定了一个规则:当一个CPU修改缓存中的字节时,服务器中其他CPU会被通知,它们的缓存将视为无效
处理器使用三个相互依赖的机制来实现加锁的原子操作:
1、保证原子操作
2、总线加锁,使用LOCK#信号和LOCK指令前缀
3、高速缓存相干性协议,确保对高速缓存中的数据结构执行原子操作(高速缓存锁)。这种机制存在于Pentium4、Intel Xeon和P6系列处理器中
synchronized实现细节
字节码
ACC_SYNCHRONIZED
monitorenter 和 monitorexit
JVM 层面
c 和 C++ 调用了操作系统提供的同步机制
OS和硬件层面
X86 : lock cmpxchg xxxx
详细细节见 JVM(四):对象的内存布局