提出java内存模型的背景:
- 一般一个复杂的运算任务,不可能是靠处理器计算就能完成,还需要进行内存交互;如读取运算数据,存储运算结果。由于处理器和存储设备的运算速度差距几个数量级,因此就在处理器和内存之间加入了速度接近处理器的高速缓存来作为处理器和内存的缓冲:运算过程产生的中间数据可以存在高速缓存,让运算可以快速进行,等运算完毕之后,再把结果从高速缓存同步回内存,这样处理器就不需要等待缓慢的内存读写。
不过这样也引出一个问题,缓存一致性问题。每个处理器都有一个高速缓存,不同高速缓存共享主内存数据,如果多个处理器的运算任务都涉及统一内存区域,可能导致缓存不一致问题。这时候需要在高速缓存和内存之间再加上一个缓存一致性协议来解决该问题。
除了增加高速缓存,为了使处理器内部的运算单元能尽量被充分利用,处理器可能对输入代码进行乱序执行优化。处理器只保证结果与原顺序执行结果一致,但是不同语句的先后顺序可能发送变化,产生的中间结果有可能不同。
java内存模型是一种虚拟机规范。目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
-
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
-
java内存模型分为主内存和工作内存,规定所有的变量存储在主内存,每条线程都有一个工作内存,操作共享数据的时候,CPU会从主内存将共享数据拷贝到工作内存,操作完毕之后再把数据更新回主内存。不同工作线程之间不能之间访问对方工作内存的变量,线程间变量值的传递需要通过主内存来完成。在程序执行过程中,可能会根据情况对字节码进行重排序优化,这时候只能确保结果和原顺序一致,不能保证中间变量也一样。
volatile
- 确保数据线程间可见性。这里的可见性是指当一条线程修改了该变量的值,新值对于其他线程来说是可以立刻得知的。
- 禁止指令重排序优化。
- volatile的可见性和防止重排序是通过内存屏障来实现的,内存屏障是被插入两个 CPU 指令之间的一种指令,用来禁止处理器指令发生重排序,从而保障有序性的。
被volatile修饰的值被修改之后,该线程会立刻将值同步回主内存,其他线程操作该变量的时候,都要去主内存获取该变量的最新版本。从而保障可见性。(happens-beofre原则规则可见性规则)
主存和工作内存的内存交互靠8 种基本操作完成,这八种操作都是原子不可再分的,不过double,long变量的load,store,read,write操作在某些平台允许有例外,即:
- lock (锁定) ,作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock (解锁) ,作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read (读取) ,作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
- load (载入) ,作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
- use (使用) ,作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时就会执行这个操作。
- assign (赋值) ,作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store (存储) ,作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后 write 操作使用。
- write (写入) ,作用于主内存的变量,它把 Store 操作从工作内存中得到的变量的值放入主内存的变量中。
在执行上面八种操作的时候必须满足以下规则:
- 规则 1:如果要把一个变量从主内存中复制到工作内存,就需要按顺序的执行 read 和 load操作,如果把变量从工作内存中同步回主内存中,就要按顺序的执行 store 和 write 操作。但 Java 内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
规则 2:不允许 read 和 load、store 和 write 操作之一单独出现。
规则 3:不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须同步到主内存中。
规则 4:不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中。
规则 5:一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign )的变量。
即对一个变量实施 use 和 store 操作之前,必须先执行过了 load 或 assign 操作。
规则 6:一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。所以 lock 和 unlock 必须成对出现。
规则 7:如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值。
规则 8:如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作;也不允许去 unlock 一个被其他线程锁定的变量。
规则 9:对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)。
内存交互基本操作有三个特性:原子性,可见性,有序性。
happens-before关系
该关系用于描述下 2 个操作的内存可见性。如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见。
happens-before有以下规则:
- 程序次序规则,一个线程内,按照代码顺序,书写在前面的操作 happens-before 书写在后面的操作。
- 锁定规则,一个 unLock 操作 happens-before 后面对同一个锁的 lock 操作。
- volatile 变量规则,对一个变量的写操作 happens-before 后面对这个变量的读操作。
- 传递规则,如果操作 A happens-before 操作 B,而操作 B 又 happens-before 操作 C,则可以得出操作 A
happens-before 操作 C。 - 线程启动规则,Thread 对象的 start() 方法 happens-before 此线程的每个一个动作。
- 线程中断规则,对线程 interrupt() 方法的调用 happens-before 被中断线程的代码检测到中断事件的发生。
- 线程终结规则,线程中所有的操作都 happens-before 线程的终止检测,我们可以通过 Thread.join()
方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行。 - 对象终结规则,一个对象的初始化完成 happens-before 它的 finalize() 方法的开始。
内存屏障插入规则
- 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。该屏障除了保证了屏障之前的写操作和该屏障之后的写操作不能重排序,还会保证了 volatile 写操作之前,任何的读写操作都会先于 volatile 被提交。
- 在每个volatile 写操作的后面插入一个 StoreLoad 屏障。该屏障除了使 volatile写操作不会与之后的读操作重排序外,还会刷新处理器缓存,使 volatile 变量的写更新对其他线程可见。
- 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。该屏障除了使 volatile读操作不会与之前的写操作发生重排序外,还会刷新处理器缓存,使 volatile 变量读取的为最新值。
- 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。该屏障除了禁止了 volatile读操作与其之后的任何写操作进行重排序,还会刷新处理器缓存,使其他线程 volatile 变量的写更新对 volatile 读操作的线程可见。
参考:
《深入理解java虚拟机 第三版》