概述
Java 内存模型(简称 JMM):定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式。
JMM 是共享内存的并发模型,线程之间主要通过读-写共享变量(堆内存中的实例域,静态域和数组元素)来完成隐式通信。JMM 控制 Java 线程之间的通信,决定一个线程对共享变量的写入何时对另一个线程可见。
注意:JMM和内存结构是不一样的,内存结构是这JVM运行时数据分区域存储,强调对内存空间的划分。
JMM与硬件内存结构关系
计算机硬件内存结构
现代计算机通常有2个或更多CPU。 其中一些CPU也可能有多个内核。 这样在具有2个或更多CPU的现代计算机上,可以同时运行多个线程。
每个CPU都能够在任何给定时间运行一个线程。每个CPU基本上都包含一组在CPU内存中的寄存器。 CPU可以在这些寄存器上执行的操作比在主存储器中对变量执行的操作快得多。
每个CPU还具有CPU高速缓存存储器层,作为内存和处理器之间的缓冲。将运算需要使用到的数据复制到缓存中,让运算能快速运行,当运算结束后再从缓存同步回内存之中。
在多处理器的系统中(或者单处理器多核的系统),每个处理器内核都有自己的高速缓存,它们共享同一主内存(Main Memory)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。
为此,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,来维护缓存的一致性。
JMM与硬件内存连接
Java内存模型和硬件内存架构是不同的。硬件内存架构不区分线程堆栈和堆。在硬件上,线程堆栈和堆都位于主存储器中。线程堆栈和堆的一部分有时可能存在于CPU高速缓存和内部CPU寄存器中。
JVM 主内存与工作内存
Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量(线程共享的变量)存储到内存和从内存中取出变量这样底层细节。
Java 内存模型中规定了所有的变量都存储在主内存中,每条线程有自己的工作内存。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
这里的工作内存是 JMM 的一个抽象概念,也叫本地内存,其存储了该线程读/写共享变量的副本。就像每个处理器内核拥有私有的高速缓存,JMM 中每个线程拥有私有的本地内存。
不同线程之间无法直接访问对方工作内存中的变量,线程间的通信一般有两种方式进行,一是通过消息传递,二是共享内存。Java 线程间的通信采用的是共享内存方式,线程、主内存和工作内存的交互关系如图:
内存间的相互操作
Java内存模型定义了变量从主内存拷贝到工作内存,以及从工作内存如何同步回主内存的所有实现细节,一共8种操作,jvm实现时必须保证这8种操作都是原子的(仅对于double和long类型的变量,在load store read write操作在某些平台上允许有例外)。
八种基操:
- Lock:作用域- 主内存变量,把变量标识为一条线程独占
- Unlock:主内存变量,unclock锁定的变量,unlock后才能被其他线程锁定
- Read:主内存变量,将变量的值从主内存传到线程的工作内存中,以便后面load
- Load:工作内存变量,将上面得到的变量值放入工作内存的变量副本中
- Use:工作内存变量,将变量值传递给执行引擎,(只有使用到才会去获取,也就是之前说变量副本只是一个引用或者变量中的一个字段才会被复制。) 每当jvm遇到一个需要使用变量值的字节码指令时执行该操作。
- Assign:工作内存变量,把从执行引擎接收的值赋值给工作内存的变量,jvm遇到一个给变量赋值的字节码指令时执行该操作。
- Store:工作内存变量,把工作内存一个变量的值传到主内存中,以便write
- Write:主内存变量,把store操作从工作内存得到的变量值放入主内存的变量中
如果要把变量从主内存拷贝到工作内存,按顺序执行read load,反之,要把变量从工作内存同步回主内存,也要按顺序执行store和write。以上两个操作必须按顺序,但不必连续,他们之间可以插入其他指令,比如对主内存中变量a,b访问,可以是read a read b load a load b
8种基操必须满足以下规则:
- read load store write不能单独出现,从主内存读出,工作内存必须接受,反之亦然
- 不允许线程丢弃最近的assign操作,就是执行引擎一旦执行,就必须assign再同步回主内存
- 新变量只能在主内存诞生,不允许工作内存中使用未被初始化(load或assign)的变量,也就是强调了load 和use以及assign和store的不可分割性,出现use store就必须出现load assign
- 一个变量同一时刻只能被一条线程独占lock,可多次lock。但是要进行相同次数的unlock才能解锁。也就是,lock和unlock一比一才能解锁
- lock操作会去清空工作内存中此变量的值,在执行引擎使用该变量前,需要重新执行load或assign操作以初始化变量的值。
- 同4,unlock和lock是一对一的,不能对没有lock的变量unlcok,也不能拿到lock但没有unlock的变量。也不允许去unlock其他线程lock的变量
- 一个变量unlock之前必须把此变量同步回主内存中,也就是执行store write
重排序
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
为了保证内存可见性,java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM 把内存屏障指令分为下列四类:
| 屏障类型 | 指令示例 | 说明 |
|---|---|---|
| LoadLoad Barriers | Load1; LoadLoad; Load2 | 确保 Load1 数据的装载,之前于 Load2 及所有后续装载指令的装载。 |
| StoreStore Barriers | Store1; StoreStore; Store2 | 确保 Store1 数据对其他处理器可见(刷新到内存),之前于 Store2 及所有后续存储指令的存储。 |
| LoadStore Barriers | Load1; LoadStore; Store2 | 确保 Load1 数据装载,之前于 Store2 及所有后续的存储指令刷新到内存。 |
| StoreLoad Barriers | Store1; StoreLoad; Load2 | 确保 Store1 数据对其他处理器变得可见(指刷新到内存),之前于 Load2 及所有后续装载指令的装载。 |
StoreLoad Barriers 会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。
StoreLoad Barriers 是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer fully flush)。
happens-before
从JDK5开始,Java 内存模型提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。
如果一个操作执行的结果需要对另一个操作可见这里的“可见”是指当一条线程修改了某个变量的值,新值对于其他线程来说是可以立即得知的,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
如果A happens-before B,那么 Java 内存模型将向程序员保证A操作的结果将对B可见,且A的执行顺序排在B之前。
重要的 happens-before 规则如下:
- 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
- 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
- volatile变量规则:对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。
- 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。 happens-before 与 JMM 的关系如下图所示:
如上图所示,一个 happens-before 规则通常对应于多个编译器重排序规则和处理器重排序规则。对于 java 程序员来说,happens-before 规则简单易懂,它避免程序员为了理解 JMM 提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现。