首页 > 理解 JVM 实现可见性和禁止指令重排的原理
头像
自从厌倦于追寻
编辑于 2020-07-25 21:20
+ 关注

理解 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 不能访问这个缓存行。

    能力有限,欢迎指正……

全部评论

(4) 回帖
加载中...
话题 回帖

推荐话题

相关热帖

近期精华帖

热门推荐