什么是 JMM
- JMM 为 JAVA 内存模型(Java Memory Model),不存在的东西,是概念,是约定
- JMM 是内存分配的一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的原子性、可见性以及有序性问题
- JMM 是为了消除在各种不同的操作系统上对内存操作差异的一个规范
为什么需要 JMM
- 编程语言可以复用操作系统层面的内存模型,不同的操作系统内存模型不同
- 如果直接复用操作系统层面的内存模型,可能导致同样一套代码换一个操作系统就无法执行
- Java 语言是跨平台的,需要自己提供一套内存模型以屏蔽系统差异
CPU 缓存模型
-
为什么要弄一个 CPU 高速缓存
① 类比开发网站后台系统使用的缓存(比如 Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题
② CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题
③ CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题 -
CPU 缓存模型图
-
现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache;有些 CPU 可能还有 L4 Cache,并不常见
-
CPU Cache 的工作方式
① 先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中
② 但这样存在内存缓存不一致性的问题:比如执行一个 i++ 操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做 1++ 运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3
③ CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议(比如 MESI 协议)或者其他手段来解决;这个缓存一致性协议指的是在 CPU 高速缓存与主内存交互的时候需要遵守的原则和规范;不同的 CPU 中,使用的缓存一致性协议通常也会有所不同 -
程序运行在操作系统之上,操作系统屏蔽了底层硬件的操作细节,将各种硬件资源虚拟化;于是,操作系统也就同样需要解决内存缓存不一致性问题
-
操作系统通过内存模型(Memory Model)定义一系列规范来解决这个问题;无论是 Windows 系统,还是 Linux 系统,都有特定的内存模型
指令重排序
-
为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序
-
什么是指令重排序
简单来说就是系统在执行代码的时候并不一定是按照研发写的代码的顺序依次执行 -
常见的指令重排序有下面 2 种情况
① 编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序
② 指令并行重排:现代处理器采用指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
③ 内存系统也会有"重排序",但又不是真正意义上的重排序;在 JMM 里表现为主存和本地内存的内容可能不一致,进而导致程序在多线程下执行可能出现问题 -
Java 源代码会经历 编译器优化重排 ⇒ \Rightarrow ⇒ 指令并行重排 ⇒ \Rightarrow ⇒ 内存系统重排 的过程,最终才变成操作系统可执行的指令序列
-
指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题
-
编译器和处理器的指令重排序的处理方式不一样
① 对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序
② 对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序
③ 指令并行重排和内存系统重排都属于是处理器级别的指令重排序
JMM 如何抽象线程和主内存的关系
-
JMM 抽象了线程和主内存之间的关系,比如说线程之间的共享变量必须存储在主内存中
-
在 JDK1.2 之前,JMM 实现总是从 主(即共享内存)读取变量,是不需要进行特别的注意的
-
但在当前的 Java 内存模型下,线程可以把变量保存 本地内存 (比如机器的寄存器)中,而不是直接在主存中进行读写
-
这就可能造成一个线程在主存中修改一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致
-
主内存和本地内存
主内存:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)
本地内存:每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存 (本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本) -
Java 内存模型抽象图
-
从上图来看,线程 1 与线程 2 之间如果要进行通信的话,必须要经历下面 2 个步骤:
① 线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去
② 线程 2 到主存中读取对应的共享变量的值
也就是说,JMM 为共享变量提供了可见性的保障 -
不过,多线程下,对主内存中的一个共享变量进行操作有可能诱发线程安全问题:
① 线程 1 和线程 2 分别对同一个共享变量进行操作,一个执行修改,一个执行读取
② 线程 2 读取到的是线程 1 修改之前的值还是修改后的值并不确定,都有可能,因为线程 1 和线程 2 都是先将共享变量从主内存拷贝到对应线程的工作内存中 -
关于主内存与工作内存直接的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,Java 内存模型定义来以下八种同步操作:
① 锁定(lock): 作用于主内存中的变量,将他标记为一个线程独享变量
② 解锁(unlock):作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定
③ read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用
④ load(载入):把 read 操作从主内存中得到的变量值放入工作内存的变量的副本中
⑤ use(使用):把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令
⑥ assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
⑦ store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用
⑧ write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中 -
除了这 8 种同步操作之外,还规定了下面这些同步规则来保证这些同步操作的正确执行:
① 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中
② 一个新的变量只能在主内存中 “诞生”,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量,换句话说就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作
③ 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁
④ 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要新执行 load 或 assign 操作初始化变量的值
⑤ 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量
…
Java 内存区域和 JMM 有何区别
- JVM 内存结构和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例
- Java 内存模型和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的