JMM到底如何理解?JMM与MESI到底有没有关系? - 知乎 (zhihu.com)
不同架构的物理机器可以拥有不一样的内存模型,而JVM 也实现了“Java内存模型”,来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。(也是Java实现跨平台性的一个重要实现)
JMM 是 JVM 自己抽象实现的,用来实现Java的多线程,所以JMM 的实现紧紧围绕并发编程的三大特性:原子性、可见性、有序性 来实现。
JMM 规定了 工作内存 + 主内存 的一种模型(用于Java多线程之间的通信)。把线程之间所有的共享变量都存储到主内存中,每个线程都有自己的工作内存,线程的工作内存中使用的共享变量都是来自主内存中的副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据,是否实时触发回写机制(将工作内存中的修改及时同步到主内存)来实现各线程对共享变量的一致性(可见性)由这个共享变量是否被volatile修饰来决定。
在说三大特性之前先介绍一下JMM 工作内存和主内存之间是如何进行数据交互的
·lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
·unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量
才可以被其他线程锁定。
·read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以
便随后的load动作使用。
·load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的
变量副本中。
·use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚
拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
·assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,
每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
·store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随
后的write操作使用。
·write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的
变量中。
具体过程:
以上就是JMM 定义的 8 种操作,JVM 在实现时必须保证每种操作都是原子的。
JMM 三大特性
原子性
JMM 保证来直接保证的原子操作是read、load、use、assign、store、write这六个,也就是说,对于基本数据类型的访问、读写都是原子性的。如果我们需要保证一个更大范围的原子性保证,JMM 还提供了 lock、unlock原子操作,只不过JVM 并没有把这两个操作直接开放给用户使用,但是却提供了更高级别的字节码指令monitorenter和monitorexit来隐式的使用这两个操作,在Java代码层面就是synchronized关键字。
可见性和有序性都是由volatile关键字来保证的,而volatile又是通过内存屏障来实现的,对于增加了volatile关键字修饰的共享变量,JVM虚拟机会自动插入内存屏障,而内存屏障底层是通过汇编lock指令来实现的
lock汇编指令作用(也就是内存屏障的作用):
- 会将当前处理器缓存行的数据立即写回到系统内存(保证了可见性)
- 这个写回内存的操作会引起在在其他CPU里缓存了该内存地址的数据无效(MESI协议)
- 提供内存屏障功能,使lock前后指令不能重排序(保证了有序性)
Java规范定义的内存屏障
可见性
当JVM 发现变量被volatile修饰,volatile变量的修改需要及时的回写到主内存(lock汇编指令的作用),回写到主内存时要通过总线来进行传输,这时就可以借助MESI缓存一致性协议通过总线嗅探来监控其他所有线程的工作内存中拥有该变量的缓存行,将该缓存行失效,当其他线程再次使用该变量时就需要访问主内存中的最新数据,从而实现可见性。
有序性
导致出现有序性问题是因为JVM 通过指令重排序对代码进行优化可能导致多线程下出现问题。volatile 是如何实现的呢?通过内存屏障(lock汇编指令的作用)。
Java规定的volatile需要实现的内存屏障
在 “volatile写操作” 之前加入StoreStore屏障保证 “volatile写操作” 之前的变量的读操作不会和 “volatile写操作” 之后的读操作发生重排序,其他类似。