Java内存模型
关于 Java 内存模型,涉及的内容会很多,所以建议胖友看如下的 《深入Java内存模型.pdf》 这本小书。
然后,看完之后你肯定会忘记,就可以靠 《《深入理解 Java 内存模型》读书笔记》 来补刀。
再另外,《深入拆解 Java 虚拟机》 的 「第五部分 高效并发」 也推荐阅读。
什么是 Java 内存模型?
Java 虚拟机规范中试图定义一种 Java 内存模型(Java Memory Model,JMM)来屏蔽掉各层硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
Java 内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间的变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的关系如下图:
线程、主内存、工作内存
艿艿:当然,有个面试官会把 Java 内存模型,和 JVM 内存结构搞混淆。所以,在回答之前,可以先和面试官确认下说的是哪个。
关于 JVM 内存结构的面试题,我们在 《精尽 Java【虚拟机】面试题》 中在详细分享。
两个线程之间是如何通信的呢?
线程之间的通信方式,目前有共享内存和消息传递两种。
1)共享内存
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。
共享内存
例如上图线程 A 与 线程 B 之间如果要通信的话,那么就必须经历下面两个步骤:
- 首先,线程 A 把本地内存 A 更新过得共享变量刷新到主内存中去。
- 然后,线程 B 到主内存中去读取线程 A 之前更新过的共享变量。
2)消息传递
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。在 Java 中典型的消息传递方式,就是 #wait() 和 #notify() ,或者 BlockingQueue 。
消息传递
为什么代码会重排序?
在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
- 在单线程环境下不能改变程序运行的结果。
- 存在数据依赖关系的不允许重排序
需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。
什么是内存模型的 happens-before 呢?详细看 《【死磕 Java 并发】—– Java 内存模型之 happens-before》 文章。
什么是内存屏障?
内存屏障,又称内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。
🦅 内存屏障为何重要?
对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。当数据是不可变的,同时/或者数据限制在线程范围内,这些优化是无害的。如果把这些优化与对称多处理(symmetric multi-processing)和共享可变状态(shared mutable state)结合,那么就是一场噩梦。
当基于共享可变状态的内存操作被重新排序时,程序可能行为不定。一个线程写入的数据可能被其他线程可见,原因是数据写入的顺序不一致。适当的放置内存屏障,通过强制处理器顺序执行待定的内存操作来避免这个问题。


