27、【对线面试官】深入浅出Java内存模型
上一次已经问过了为什么要有Java内存模型
- 答案是:Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,保证了Java程序在各种平台下对内存的访问都能得到一致效果
- 强调下:Java内存模型它是一种「规范」,Java虚拟机会实现这个规范。
先聊下Java内存模型的抽象结构?
- Java内存模型定义了:Java线程对内存数据进行交互的规范。
- 线程之间的「共享变量」存储在「主内存」中,每个线程都有自己私有的「本地内存」,「本地内存」存储了该线程以读/写共享变量的副本。
- 本地内存是Java内存模型的抽象概念,并不是真实存在的。
- Java内存模型规定了:线程对变量的所有操作都必须在「本地内存」进行,「不能直接读写主内存」的变量
- Java内存模型定义了8种操作来完成「变量如何从主内存到本地内存,以及变量如何从本地内存到主内存」
- 分别是read/load/use/assign/store/write/lock/unlock操作
- 对变量一个读写操作就涵盖这些操作
happen-before规则
按我的理解下,happen-before实际上也是一套「规则」。Java内存模型定义了这套规则,目的是为了阐述「操作之间」的内存「可见性」
- 从上次讲述「指令重排」就提到了,在CPU和编译器层面上都有指令重排的问题。
但:在某些重要的场景下,这一组操作都不能进行重排序,「前面一个操作的结果对后续操作必须是可见的」。
Java内存模型就提出了happen-before这套规则,规则总共有8条
- 比如传递性、volatile变量规则、程序顺序规则、监视器锁的规则…
有了happen-before这些规则。我们写的代码只要在这些规则下,前一个操作的结果对后续操作是可见的,是不会发生重排序的。
volatile内存语义
volatile是java的一个关键字
特性:可见性和有序性(禁止重排序)
java内存模型这个规范,很大程度下就为了解决可见性和有序性的问题。
volatile是怎么做到可见性和有序性的
为了实现volatile有序性和可见性,定义了4种内存屏障的「规范」,
分别是LoadLoad/LoadStore/StroreLoad/StoreStrore
本质上,就是在volatile前后加上了内存屏障,使得编译器和CPU无法进行重排序,致使有序,并且对volatile变量对其他线程可见
Hotspot虚拟机实现
- 在「汇编」层面上实际是通过Lock前缀指令来实现的(lock支持大部分平台,而fence指令是x86平台的)
- locK指令能保证:禁止CPU和编译器的重排序(保证了有序性)、保证CPU写核 心的指令可以立即生效且其他核心的缓存数据失效(保证了可见性)。
volatile和MESl协议是啥关系?
- 没有直接关联
- Java内存模型关注的是编程语言层面上,它是高维度的抽象。
- MESI是CPU缓存一致性协议,不同的CPU架构都不一样,可能有的CPU压根就没用MESI协议.
- 只不过MESI名声大,大家就都拿他来举例子了。
- MESI可能只是在「特定的场景下」为实现volatile的可见性/有序性而使用到的一部分罢了
- 为了让Java程序员屏蔽上面这些底层知识,快速地入门使用volatile变量
- Java内存模型的happen-before规则中就有对volatile变量规则的定义:对一个volatile变量的写操作相对于后续对这个volatile变量的读操作可见
- 只要变量声明了volatile关键字,写后再读,读必须可见写的值。(可见性、有序性)
总结
为什么存在Java内存模型:Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,保证了Java程序在各种平台下对内存的访问都能得到一致效果
Java内存模型抽象结构:线程之间的「共享变量」存储在「主内存」中,每个线程都有自己私有的「本地内存」,「本地内存」存储了该线程以读/写共享变量的副本。线程对变量的所有操作都必须在「本地内存」进行,而「不能直接读写主内存」的变量
happen-before规则:Java内存模型规定在某些场景下(一共8条),前面一个操作的结果对后续操作必须是可见的。这8条规则成为happen-before规则
volatile:volatile是Java的关键字,修饰的变量是可见性且有序的(不会被重排序)。可见性&&有序性,由Java内存模型定义的「内存屏障」完成,实际HotSpot虚拟机实现Java内存模型规范,汇编底层是通过Lock指令来实现。