本文开始死磕 JMM(Java 内存模型) 由于知识点较多,分来写
该文为 JMM 第一篇
技术往往是枯燥的,本文文字较多
1. JMM 是什么?
其实 JMM 很好理解,我简单的解释一下,在 Java 多线程中我们经常会涉及到两个概念就是线程之间是如何通信和线程之间的同步,那什么是线程之间的通信呢,其实就是两个线程之间互相交换信息线程之间通信的方式共有两种: 一种就是共享内存,和消息传递
。在共享内存中的并发模型中线程是通过读取主内存的共享信息来进行隐性通信的。在消息传递通信中线程之间没有公共的状态,只能通过发送消息来进行显性通信。然而这只是线程通信,那么同步呢,同步就是在多线程的情况下有顺序的去执行。在共享内存中同步时显式进行的,在代码中我们必须要去指定方法需要同步执行比如说加同步锁等。在消息传递的并发模型中发送消息必须是在消接收之前,所以同步时隐式的。
2. 为什么要涉及到线程并发通信
java 内存模型其实可以说是 Java 并发内存模型,在 Java 中是采用的共享内存模型的方式,所以 Java 线程之间的通信是隐式进行的,对我们是完全透明的,如果你不了解通信机制的话会产生各种线程可见性的问题。其实在 Java 中所有的静态域,域和数组元素都存在堆内存中,堆内存在线程中是共享的一般我们都称之为共享变量,局部变量,方法定义参数和异常处理参数不会在线程中共享, 所以不会存在线程可见性的问题。上面我就说过线程之间的通信是由 JMM 来进行控制的,JMM 来决定了一个线程操作了共享变量后如何对另一个线程可见。从上面所说的概念来看的话,JMM 定义了线程与主内存的关系。
3.JMM 规定
其实这样做的原因就是 Java 是跨平台语言,在个操作系统中内存都有一定的差异性,这样久造成了并发不一致,所以 JMM 的作用就是用来屏蔽掉不同操作系统中的内存差异性来保持并发的一致性。同时 JMM 也规范了 JVM 如何与计算机内存进行交互。简单的来说 JMM 就是 Java 自己的一套协议来屏蔽掉各种硬件和操作系统的内存访问差异,实现平台一致性达到最终的 ” 一次编写,到处运行 “,说了这么多,JMM 到底是怎么控制的呢?然后如何通信的呢?我们继续往下看。
4. 模型
JMM 是一个抽象的概念,并不是真实的存在,它涵盖了缓冲区,寄存器以及其他硬件和编译器优化。
Java 内存模型抽象图如下:
从上图可以看出每个线程都有一个本地内存,如果线程想要通信的话要执行一下步骤:
- A 线程先把本地内存的值写入主内存
- B 线程从主内存中去读取出 A 线程写的值
再看下面的这个图,表示了 A 如何向 B 发送消息
假设这时候有一个共享变量 X 默认值都是为 0,那么线程 A 把 X 的值修改为 1, 这时候如何才能同步到 B 线程呢。
如果 A 线程把 X 修改成 1 之后,A 线程会把 X 从 A 的本地内存中写入到主内存中,这样的话主内存的 X 就等于 1 了,这时候 B 线程就会去读取主内存的 X 变量,存入 B 的本地内存中,这样 B 线程的 X 变量值也就会变成了 1。这样对吗。那现在如何通信我是知道了关键它究竟是如何来实现的,就是如何来实现通信的呢?
5. 通信
上面所说的步骤其实就是实现了线程之间的通信,但是不要以为线程之间的通信就是这么简单的,其实在 Java 中 JMM 内存模型定义了八种操作来实现同步的细节。
- read 读取,作用于主内存把变量从主内存中读取到本本地内存。
- load 加载,主要作用本地内存,把从主内存中读取的变量加载到本地内存的变量副本中
- use 使用,主要作用本地内存,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。、
- assign 赋值 作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store 存储 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作。
- write 写入 作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中。
- lock 锁定:作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock 解锁:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
所以看似简单的通信其实是这八种状态来实现的。
同时在 Java 内存模型中明确规定了要执行这些操作需要满足以下规则:
- 不允许 read 和 load、store 和 write 的操作单独出现。
- 不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须同步到主内存中。
- 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中。
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量。即就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作。
- 一个变量在同一时刻只允许一条线程对其进行 lock 操作,lock 和 unlock 必须成对出现
- 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值
- 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作;也不允许去 unlock 一个被其他线程锁定的变量。
- 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)。
所以上面说的操作要严格执行。
目前写了这么多,下文预告:
LinkedHashMap 源码分析
参考资料《深入 Java 内存模型》