为了更好的理解 Java 是如何实现 按需禁用缓存和编译优化 的,我们首先需要对 Java 的内存模型有一个初步的了解。
Java 内存模型主要由以下三部分构成:1 个主内存、n 个线程、n 个工作内存(与线程一一对应),数据就在它们三者之间来回倒腾。那么怎么倒腾呢?靠的是 Java 提供给我们的 8 个原子操作:lock 、unlock 、read 、load 、use 、assign 、store 、write ,其操作流程示意图如下:
一个变量从主内存拷贝到工作内存,再从工作内存同步回主内存的流程为:
|主内存| -> read -> load -> |工作内存| -> use -> |Java线程| -> assign -> |工作内存| -> store -> write -> |主内存|
Java 内存模型中的 8 个原子操作
lock :作用于主内存,把一个变量标识为一个线程独占状态。read :作用于主内存,把一个变量的值从主内存传输到线程工作内存中,供之后的 load 操作使用。load :作用于工作内存,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。use :作用于工作内存,把工作内存中的一个变量传递给执行引擎,虚拟机遇到使用变量值的字节码指令时会执行。assign :作用于工作内存,把一个从执行引擎得到的值赋给工作内存的变量,虚拟机遇到给变量赋值的字节码指令时会执行。store :作用于工作内存,把工作内存中的一个变量传送到主内存中,供之后的 write 操作使用。write :作用于主内存,把 store 操作从工作内存中得到的变量值存入主内存的变量中。unlock :作用于主内存,释放一个处于锁定状态的变量。
8 个原子操作的执行规则
有关变量拷贝过程的规则
- 不允许
read 和 load ,store 和 write 单独出现 - 不允许线程丢弃它最近的
assign 操作,即工作内存变化之后必须把该变化同步回主内存中 - 不允许一个线程在没有
assign 的情况下将工作内存同步回主内存中,也就是说,只有虚拟机遇到变量赋值的字节码时才会将工作内存同步回主内存 - 新的变量只能从主内存中诞生,即不能在工作内存中使用未被
load 和 assign 的变量,一个变量在 use 和 store 前一定先经过了 load 和 assign
有关加锁的规则
- 一个变量在同一时刻只允许一个线程对其进行
lock 操作,但是可以被一个线程多次 lock (锁的可重入) - 对一个变量进行
lock 操作会清空这个变量在工作内存中的值,然后在执行引擎使用这个变量时,需要通过 assign 或 load 重新对这个变量进行初始化 - 对一个变量执行
unlock 前,必须将该变量同步回主内存中,即执行 store 和 write 操作 - 一个变量没有被
lock ,就不能被 unlock ,也不能去 unlock 一个被其他线程 lock 的变量
可见性问题 -> 有序性问题
通过上图可以发现,Java 线程只能操作自己的工作内存,其对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存中的变量。这就有可能会导致可见性问题:
- 因为对于主内存中的变量 A,其在不同的线程的工作内存中可能存在不同的副本 A1、A2、A3。
- 不同线程的
read 和 load 、store 和 write 不一定是连续执行的,中间可以插入其他命令。Java 只能保证 read 和 load 、store 和 write 的执行对于一个线程而言是连续的,但是并不保证不同线程的 read 和 load 、store 和 write 的执行是连续的,如下图: 假设有两个线程 A 和 B,其中线程 A 在写入共享变量,线程 B 要读取共享变量,我们想让线程 A 先完成写入,线程 B 再完成读取。此时即便我们是按照 “线程 A 写入 -> 线程 B 读取” 的顺序开始执行的,真实的执行顺序也可能是这样的:storeA -> readB -> writeA -> loadB ,这将导致线程 B 读取的是变量的旧值,而非线程 A 修改过的新值。也就是说,线程 A 修改变量的执行先于线程 B 操作了,但这个操作对于线程 B 而言依旧是不可见的。 那么如何解决这个问题呢?通过上述的分析可以发现,可见性问题的本身,也是由于不同线程之间的执行顺序得不到保证导致的,因此我们也可以将它的解决和有序性合并,即对 Java 一些指令的操作顺序进行限制,这样既保证了有序性,有解决了可见性。
于是乎,Java 给出了一些命令执行的顺序规范,也就是大名鼎鼎 Happens-Before 规则。
|