Java并发编程(三)-内存屏障

Scroll Down

Java并发编程(三)-内存屏障

锁在保证可见性的时候是在线程获得和释放锁的时候分别执行两个动作:刷新处理器缓存和冲刷处理器缓存.而JAVA虚拟机实际上是借助内存屏障(Memory Barrier,也称Fence)来实现这两个动作的.内存屏障是被插入到两个指令之间进行使用的,是用来禁止编译器,处理器重排序从而保障有序性.它在指令序列中像是一道墙(屏障)使其两侧的指令无法穿越它(一旦穿越就是重排序).为了实现禁止重排序的功能,这些指令往往都会具有一个副作用-刷新处理器缓存,冲刷处理器缓存.

内存屏障-保障可见性可分为加载屏障(Load Barrier)和存储屏障(Store Barrier).

  • 加载屏障的作用是刷新处理器缓存,存储屏障的作用是冲刷处理器缓存.
  • JAVA虚拟机会在MonitorExit(释放锁的时候)对应的机器码指令后插入一个存储屏障,保障写线程在释放锁之前对在临界区中修改共享变量的值能够同步到处理器的高速缓存中
  • 在MonitorEnter(获得锁)的机器码之后进入临界区之前插入一个加载屏障,使得线程能够将写线程对相应共享变量所做的更新从其他处理器同步到该处理器的高速缓存中.

内存屏障-保障有序性可分为获取屏障(Acquire Barrier)和释放屏障(Release Barrier)

  • 获取屏障的使用方式是在读操作之后插入该内存屏障,作用是禁止该读操作和其后任何操作之间进行重排序,相当于之后任何操作都先要获得共享数据的所有权
  • 释放屏障是在写操作之前插入该屏障,作用是禁止该写操作与前面的任何读写操作之间进行重排序.

JAVA内存屏障使用示意图:

image-20200323222124641

由于获取屏障禁止了临界区中任何读、写操作被重排序到临界区之前的可能性,而释放屏障又禁止了临界区任何读、写操作被重排序到临界区之后的可能新,因此临界区内任何读、写操作都无法被重排序到临界区之外.在锁的排他性作用下,临界区代码执行具有原子性,而由于临界区中的代码都是一下子更新的,读线程对于写线程执行临界区中的代码也是一致的,所以自然而然具有有序性.

锁与重排序

临界区外的代码是可以被编译器重排序到临界区之内(许进),而临界区内的操作无法被重排到临界区之外(不许出),临界区内的代码只要满足貌似串行语义,也是可以进行重排序的.

临界区的代码重排序规则需要遵守以下规则:

  • 临界区内的操作不能重排序到临界区之外
  • 临界区内的操作之间允许重排序
  • 临界区外的操作之间可以重排序
  • 锁申请与锁释放的操作不能重排序
  • 两个锁申请操作不能重排序
  • 两个锁释放操作不能重排序
  • 临界区外的代码可以被重排序到临界区之内