文章目录
- 什么是JMM?
- 现代计算机内存模型
- 缓存一致性
- JMM内存模型与计算机内存模型的关系
- 线程间通信
- JMM三大问题
- 原子性
- 可见性
- 有序性
什么是JMM?
JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。 JMM可以理解为是一个规范,一个抽象概念,并不真实存在。
现代计算机内存模型
现代计算机中,CPU的指令速度远远超过内存的存取速度,由于计算机的存储设备与CPU的运算速度有几个数量级的差距,所以现在计算机中都不得不加入一层读写速度尽可能接近CPU运算速度的高速缓存(cache)来作为内存和CPU之间的缓冲。
基于高速缓存的存储交互很好的解决了CPU和内存的速度的矛盾,但也引入了一个新的问题,缓存一致性,在多处理器系统中,每个CPU都有自己的高速缓存,而他们又共享同一主内存,当多个处理器运算任务都涉及到同一块主内存区域时,将可能导致各自的缓存数据不一致。为了解决这个问题,需要各个处理器在访问内存时,需要遵循一些协议,例如MSI、EMSI、MOSI等。
缓存一致性
为了解决这个问题,先后有过两种办法
总线锁机制
总线锁就是使用CPU提供的一个LOCK#信号,当一个处理器在总线上输出此信号,其他处理器的请求将被阻塞,那么该处理器就可以独占共享锁。这样就保证了数据一致性。
缓存锁机制
但是总线锁定开销太大,我们需要控制锁的力度,所以又有了缓存锁,核心就是缓存一致性协议,不同的CPU硬件厂商实现方式稍有不同,有MSI、MESI、MOSI等。
JMM内存模型与计算机内存模型的关系
JVM虚拟机是一种抽象化的计算机,同计算机一样,它有着自己的一套完善的硬体架构,如处理器、堆栈、寄存器、操作指令等,而在JVM篇中也讲到过虚拟机栈,虚拟机栈是用于描述java方法执行的内存模型,因此JMM也是属于JVM的一部分,只是JMM是一种抽象的概念,是一组规则,并不实际存在。所不同的是,JMM模型定义的内存分为工作内存和主内存,工作内存是从主内存拷贝的副本,属于线程私有。当线程启动时,从主内存中拷贝副本到工作内存,执行相关指令操作,最后写回主内存。
线程间通信
为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现
-
lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。
-
unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
-
read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
-
load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
-
use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
-
assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
-
store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
-
write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
JMM对这八种指令的使用,制定了如下规则:
-
不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
-
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
-
不允许一个线程将没有assign的数据从工作内存同步回主内存
-
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
-
一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
-
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
-
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
-
对一个变量进行unlock操作之前,必须把此变量同步回主内存
JMM三大问题
原子性
一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。
如何解决
- synchronized
- Lock锁
- 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作
- JDK提供了很多原子操作类来保证操作的原子性,例如基础类型:AtomicInteger,引用类型AtomicReference等。
可见性
在多线程环境下,一个线程对共享变量的修改,不仅要对本线程可见,而且要对其他线程可见。
如何解决
- synchronized
- Lock锁
- JDK提供了很多原子操作类来保证操作的原子性,例如基础类型:AtomicInteger,引用类型AtomicReference等。
- volatile: 使用volatile关键字修饰一个变量可以保证变量的可见性
volatile如何保证可见性
- 线程对共享变量的副本做了修改,会立刻刷新最新值到主内存中。
- 线程对共享变量的副本做了修改,其他其他线程中对这个变量拷贝的副本会时效;
- 其他线程如果需要对这个共享变量进行读写,必须重新从主内存中加载。
有序性
程序执行的顺序按照代码的先后顺序执行
如何解决
happens-before原则: happens-before原则是java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,也就是说发生操作B之前,操作A产生的影响能被操作B观察到。这里的“影响”包括修改共享变量,方法调用。详细的happens-before说明请参看happens-before原则章节。
synchronized机制: synchronized能够保证有序性是因为synchronized可以保证同一时间只有一个线程访问代码块,而单线程环境下,JMM能够保证代码的串行语义;虽然使用synchronized的代码块,还可以发生指令重排序,但是synchronized可以保证只有一个线程执行,所以最后的结果还是正确的。
volatile机制: volatile的底层是使用内存屏障来保障有序性的。写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后。读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。