Java 内存模型(JMM)面试答复,包过!
在面试中,面试官常常喜爱问:『说说什么是 Java 内存模型(JMM)?』
面试者心田狂喜,这题刚背过:『Java 内存次要分为五大块:堆、办法区、虚拟机栈、本地办法栈、PC 寄存器,balabala……』
面试官会心一笑,露出一道光辉:『好了,明天的面试先到这里了,回去等告诉吧』
个别听到等告诉这句话,这场面试大概率就是凉凉了。为什么呢?因为面试者弄错了概念,面试官是想考查 JMM,然而面试者一听到 Java 内存这几个关键字就开始背诵八股文了。Java 内存模型 (JMM) 和 Java 运行时内存区域区别可大了呢,不要走开接着往下看,许可我要看完。
1. 为什么要有内存模型?
要想答复这个问题,咱们须要先弄懂传统计算机硬件内存架构。好了,我要开始画图了。
1.1. 硬件内存架构
1)CPU
去过机房的同学都晓得,个别在大型服务器上会配置多个 CPU,每个 CPU 还会有多个核,这就意味着多个 CPU 或者多个核能够同时(并发)工作。如果应用 Java 起了一个多线程的工作,很有可能每个 CPU 都会跑一个线程,那么你的工作在某一刻就是真正并发执行了。
(2)CPU Register
CPU Register 也就是 CPU 寄存器。CPU 寄存器是 CPU 外部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级。
(3)CPU Cache Memory
CPU Cache Memory 也就是 CPU 高速缓存,绝对于寄存器来说,通常也能够成为 L2 二级缓存。绝对于硬盘读取速度来说内存读取的效率十分高,然而与 CPU 还是相差数量级,所以在 CPU 和主存间引入了多级缓存,目标是为了做一下缓冲。
(4)Main Memory
Main Memory 就是主存,主存比 L1、L2 缓存要大很多。
留神:局部高端机器还有 L3 三级缓存。
1.2. 缓存一致性问题
因为主存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存来作为主存和处理器之间的缓冲,CPU 将罕用的数据放在高速缓存中,运算完结后 CPU 再讲运算后果同步到主存中。
应用高速缓存解决了 CPU 和主存速率不匹配的问题,但同时又引入另外一个新问题:缓存一致性问题。
在多 CPU 的零碎中(或者单 CPU 多核的零碎),每个 CPU 内核都有本人的高速缓存,它们共享同一主内存(Main Memory)。当多个 CPU 的运算工作都波及同一块主内存区域时,CPU 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不统一。
因而须要每个 CPU 拜访缓存时遵循肯定的协定,在读写数据时依据协定进行操作,独特来保护缓存的一致性。这类协定有 MSI、MESI、MOSI、和 Dragon Protocol 等。
1.3. 处理器优化和指令重排序
为了晋升性能在 CPU 和主内存之间减少了高速缓存,但在多线程并发场景可能会遇到 缓存一致性问题
。那还有没有方法进一步晋升 CPU 的执行效率呢?答案是:处理器优化。
为了使处理器外部的运算单元可能最大化被充分利用,处理器会对输出代码进行乱序执行解决,这就是处理器优化。
除了处理器会对代码进行优化解决,很多古代编程语言的编译器也会做相似的优化,比方像 Java 的即时编译器(JIT)会做指令重排序。
处理器优化其实也是重排序的一种类型,这里总结一下,重排序能够分为三种类型:
- 编译器优化的重排序。编译器在不扭转单线程程序语义放入前提下,能够重新安排语句的执行程序。
- 指令级并行的重排序。古代处理器采纳了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器能够扭转语句对应机器指令的执行程序。
- 内存零碎的重排序。因为处理器应用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
2. 并发编程的问题
下面讲了一堆硬件相干的货色,有些同学可能会有点懵,绕了这么大圈,这些货色跟 Java 内存模型有啥关系吗?不要急咱们缓缓往下看。
相熟 Java 并发的同学必定对这三个问题很相熟:『可见性问题』、『原子性问题』、『有序性问题』。如果从更深层次看这三个问题,其实就是下面讲的『缓存一致性』、『处理器优化』、『指令重排序』造成的。
缓存一致性问题其实就是可见性问题,处理器优化可能会造成原子性问题,指令重排序会造成有序性问题,你看是不是都分割上了。
出了问题总是要解决的,那有什么方法呢?首先想到简略粗犷的方法,干掉缓存让 CPU 间接与主内存交互就解决了可见性问题,禁止处理器优化和指令重排序就解决了原子性和有序性问题,但这样一夜回到解放前了,显然不可取。
所以技术前辈们想到了在物理机器上定义出一套内存模型,标准内存的读写操作。内存模型解决并发问题次要采纳两种形式:限度处理器优化和应用内存屏障。
3. Java 内存模型
同一套内存模型标准,不同语言在实现上可能会有些差异。接下来着重讲一下 Java 内存模型实现原理。
3.1. Java 运行时内存区域与硬件内存的关系
理解过 JVM 的同学都晓得,JVM 运行时内存区域是分片的,分为栈、堆等,其实这些都是 JVM 定义的逻辑概念。在传统的硬件内存架构中是没有栈和堆这种概念。
从图中能够看出栈和堆既存在于高速缓存中又存在于主内存中,所以两者并没有很间接的关系。
3.2. Java 线程与主内存的关系
Java 内存模型是一种标准,定义了很多货色:
- 所有的变量都存储在主内存(Main Memory)中。
- 每个线程都有一个公有的本地内存(Local Memory),本地内存中存储了该线程以读 / 写共享变量的拷贝正本。
- 线程对变量的所有操作都必须在本地内存中进行,而不能间接读写主内存。
- 不同的线程之间无奈间接拜访对方本地内存中的变量。
看文字太干燥了,我又画了一张图:
3.3. 线程间通信
如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 JMM 标准下会有一系列的操作。
为了更好的管制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现:
- lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,开释后的变量才能够被其余线程锁定。
- read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作应用
- load:载入。作用于工作内存的变量,它把 read 操作从主内存中失去的变量值放入工作内存的变量正本中。
- use:应用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个须要应用变量的值的字节码指令时将会执行这个操作。
- assign:赋值。作用于工作内存的变量,它把一个从执行引擎接管到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作。
- write:写入。作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中。
留神:工作内存也就是本地内存的意思。
4. 有态度的总结
因为 CPU 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 CPU 和主内间的缓冲晋升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。
数据同时存在于高速缓存和主内存中,如果不加以标准势必造成劫难,因而在传统机器上又形象出了内存模型。
Java 语言在遵循内存模型的根底上推出了 JMM 标准,目标是解决因为多线程通过共享内存进行通信时,存在的本地内存数据不统一、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
为了更精准管制工作内存和主内存间的交互,JMM 还定义了八种操作:lock, unlock, read, load,use,assign, store, write。
Java 内存模型(JMM)面试答复到此结束!你过了吗?