JMM(Java内存模型)学习
一、什么是Java内存模型? 为什么需要JMM?
JMM(Java内存模型),可以看作是Java定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从Java源代码到CPU可执行指令的这个转化过程中要遵守哪些并发相关的原则和规范,其主要目的是简化多线程编程,增强程序的可移植性。
至于为什么需要JMM呢?为什么需要遵循这些并发相关的原则和规范呢,这是因为并发编程下,像 CPU 多级缓存和指令重排这类设计可能会导致程序运行出现一些问题。就需要定义一些规范来解决这些问题,开发者可以利用这些规范更方便地开发多线程程序,保证程序的安全可靠。
二、CPU缓存 及 指令重排序 会带来什么问题呢?
(一)CPU Cache
我们知道,CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题(CPU的处理速度要远远高于内存的处理速度)。
CPU Cache 的工作方式: 先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 内存缓存不一致性的问题 !比如我执行一个 i++ 操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 i++ 运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。
CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议。
(二)指令重排序
什么是指令重排序呢? 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。举个例子:
object = new SingleObject();这段代码其实分为三步执行:
- 1、为object分配内存空间。
- 2、初始化object对象。
- 3、将object指向分配的内存地址。
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getSingleObject()后发现 object不为空,因此返回 object,但此时 object还未被初始化。
那如何防止指令重排序的产生呢?
对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。
(内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障变量的可见性。
)
三、JMM如何抽象线程和主内存之间的关系?
Java 内存模型(JMM) 抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中。
什么是主内存?什么是本地内存?
- 主内存:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)
- 本地内存:每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。
从上图来看,线程 1 与线程 2 之间如果要进行通信的话,必须要经历下面 2 个步骤:
- 线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。
- 线程 2 到主存中读取对应的共享变量的值。
也就是说,JMM 为共享变量提供了可见性的保障。