Forehead

I was thinking about translating this article into English and Japanese, but not too sure if I need to do that; it will take one hour or two after all. Please shoot me an email if you feel that would work at: leileetcodekaggle@163.com

Prelude

总线风暴,缓存雪崩,容易联想到暴雪和鲁·高因。

Inspiration

自从装上了AMD的3990X,64核128线程的怪物配置之后: CineBench测试: PBO开启之后能达到3.8GHz!

3990X-CPUz_PBO.png

前言

回归正题。Prerequisites: volatile和JMM模型相关;volatile三大特性: 可见性,原子性,有序性;其中老生常谈:

  1. 原子性不能由volatile满足, 用integer举例出现AtomicInteger,衍生出来的底层CAS还有ABA问题;
  2. 有序性实际上就是”禁止指令Assembly重排”

不过这次主要讲的是, volatile可以带来可见性。我之前以为这个没什么说的,就是描述“一个线程工作内存中对共享变量var的修改对其它线程也是可见的”; 可是深究起来,tmd凭什么可以这样?为什么其它线程就可以看到某个线程对这个var的修改?

我简单地写一下,因为不想写一大堆。

首先要知道,java代码编译之后变成class文件,也就是字节码;字节码经过JIT汇编得到Assembly汇编指令;这些指令就是机器可以识别的。(注: 后面如果要看总线锁情况下的反汇编得到的汇编指令, 需要安装hsdis/hotspot disassembler,点击这里
)

这里讨论多进程下的多线程之间的可见性问题,模型如下:
jmm.png

最下面的灰色的部分就是Main Memory; 也就是说,CPU - L1缓存 - L2缓存 - L3缓存Main Memory之间,是通过总线来进行数据交互的;这个虽然是常识,但是还是写一写避免有人不知道;

每个核上CPU工作,跑在不同CPU的线程,每个线程都有自己的工作内存。

JMM 8大原子操作

JMM 8大原子操作: load, read, use, assign, store, write, lock, unlock.工作内存从主内存读,更改值之后再写会主内存,就是按照上面写的顺序进行的。

核心

volatile保证可见性底层原理,就是java代码 -> class字节码文件 -> assembly汇编指令这个过程中,在最后得到的汇编指令集前面加上lock前缀;而lock指令,能触发缓存锁定机制, 缓存锁定机制类似一个接口,具体落地实现是缓存一致性协议或者总线锁;不过因为总线锁之于缓存一致性协议,类似JDK1.2的synchronized之于JDK1.6的synchronized:过重导致效率低, 所以现在一般实现缓存锁定机制的都是缓存一致性协议。不过总线锁,可以用hsdis加一段VM Options来看java代码对应的汇编指令:

1
2
3
4
5
-server
-Xcomp
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintAssembly
-XX:CompileCommand=compileonly,*VolatileVisibilitySample.refresh

缓存一致性协议MESI

MESI: Modified, Exclusive, Shared, Invalid

本质问题: 修改共享变量var, thread A怎么知道thread B也在修改此变量?

答: 总线嗅探Bus Snooping.

  1. 总线嗅探:thread A发现其它线程工作内存中也有var这个变量; E -> S;
  2. 某thread, e.g. thread A改变var成功,S -> M; Bus write机制: 总线写入:通知其它所有之前总线嗅探过的线程,var: M了,所以你们要放弃之前从Main Memory获得的var的值,重新去Main Memory获得新值;
  3. 当然,var修改之后,thread A会尽快把数值store到总线,再总线writeMain Memory(都是JIT 8大数据原子操作(JIT把bytecode翻译成Assembly), 便于其它线程从主内存获得最新值。

总线风暴,这里不写了。

Bibliography