理解 JVM 实现可见性和禁止指令重排的原理
在牛客看到一个关于 JVM 的 lock 、store 、 load 实现禁止指令重排和可见性的问题,本想直接在帖子下谈自己的看法,但是回帖好像限制只能 25 字……只好发个帖子……
以下为个人理解:
首先可能是作者描述不太清楚,或者我理解有误,《深入理解java虚拟机》(以及一些博客)说 lock 是一个指令前缀,将当前 CPU 的缓存写到内存里。但是我查到的 lock 前缀是这样的:The LOCK # signal is asserted during execution of the instruction following the lock prefix. This signal can be used in a multiprocessor system to ensure exclusive use of shared memory while LOCK # is asserted.
https://docs.oracle.com/cd/E19455-01/806-3773/instructionset-128/index.html
大意就是说在执行 lock 后面的指令时,将实现共享内存的互斥访问,也就是会保证其他 CPU 无能访问共享内存。
实现“互斥访问”的方式,根据查到的一些博客说,以前的 CPU 实现是通过锁住总线的方式实现,而在较新 CPU 则是通过锁住缓存的方式实现,毕竟锁总线的开销比较大,而锁缓存只需要把 lock 指令后面设计的共享内存锁住就可以了,不用锁住所有共享内存,开销比较小(锁粒度的细化)。
jvm 确实是通过添加 lock add 0x0 (%esp) 实现禁止指令重排和可见性:
指令重排不能影响程序的结果,add 指令修改了 esp 寄存器指向地址的值。如果后面有什么代码读或写这个值,肯定不能放这之前,否则会读到错误的值或写进去的值被覆盖;若前面的代码读或写这个值,同样不能放后面,否则会读到错误的值或覆盖 add 指令写进去的值。这样就禁止了指令重排。 CPU 并不能判断 add 操作是否真的会修改 esp 指向地址的值,只是 add 是写操作,就默认会修改——JVM 生成的指令add 0x0 实际上是一个空操作,也正说明了这一点,所以实际上只要是写操作,都能实现禁止重排的效果,或者说实现禁止指令重排的其实是 add 而不是 lock。
lock add 0x0 (%esp) 是给 esp 指向地址的值+0而不是《深入JVM》说的给 esp 寄存器+0,可以参考
使用括号()后,就不再是表示寄存器,而是表示寄存器指向的地址,而这个地址自然就是指向内存的地址, add 操作便是对内存一个地址 +0 ,内存的值被修改后,这个时候根据 MESI 协议什么的,CPU 缓存失效,下次 CPU 用到这个内存的时候就必须重新加载。
但这并没有实现可见性,毕竟刚刚只是给 esp 指向的内存 +0 ,而不是“在变量变化的时候让其他线程马上见到”。于是就有了 load 和 store ,在每次读变量之前都 load ,而不是使用缓存,这样变量被修改了,马上就能看到,每次修改完变量就马上刷新到内存,而不是就在缓存上,这样就实现了可见性。 上面提到禁止指令重排的其实是 add 那么为什么需要 lock 前缀呢,因为 CPU 读写缓存都是以缓存行为单位(通常是64B),一个缓存行上通常有多个变量,CPU 可能没办法一次全部复制(32 计算机应该一次只能修改 32 bit ),若 add 写内存时其他 CPU 访问共享内存就会发生不一致,因此需要使用 lock 保存 add 回写缓存行时,其他 CPU 不能访问这个缓存行。
能力有限,欢迎指正……