前言
有很多JAVA开发人员,在被问起:“你知道Java内存模型吗?”,都会回答:“知道,JAVA内存模型分为方法区、堆、…”。
…
虽说“概念”这个词发明出来是为了方便交流和说明问题的,不用太过于较真。但是,答非所问就是你的不对了。
如果对两者的概念混淆不清的,希望在看完本文后对你有所帮助。
JVM内存模型
先说什么是JVM内存模型,我相信99.99%的JAVA开发人员都知道,因为面试经常会被问到。。。
没错,就是方法区、堆、…
叫法也是五花八门,“JVM内存布局“、“JVM内存划分“又或是“JVM内存结构“,总有一个你听过的。
官方的叫法其实是“运行时数据区域”。这里是官方的说明:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5
如果想快速了解的,可以移步至Java面试中经常被问到的15道JVM面试题,里面有JVM内存相关的面试题。
那JAVA内存模型又是什么呢?
JAVA内存模型
在了解Java内存模型之前,建议先对计算机存储器的层次结构有基本的了解,特别是缓存、主存以及它们之间的交互关系。
JAVA内存模型简称:JMM(Java Memory Model),分为主内存和工作内存。
- 主内存是所有线程共享的区域,包含了所有共享变量、类信息、实例对象等,也就是运行时数据区域中的堆和方法区的数据。
- 工作内存是每个线程在执行时的私有空间,包含了局部变量表、操作数栈等,也就是运行时数据区域中的程序计数器和栈的数据。
如果看过官方的文档,你会发现对内存模型的描述是在线程和锁的章节中,可见对其描述的目的。
感兴趣的可以去官方文档瞅一瞅
简而言之,JAVA内存模型解释了在多线程编程中对共享数据访问和修改会产生的一些问题和原因,以及如何解决这些问题。
下面一一说明。
JAVA内存模型解释的问题
可见性问题
先了解一下线程执行时主内存和工作内存是如何交互的:
线程在执行时,会将主内存中的数据复制到自己的工作内存中进行操作,操作完成后再将结果刷新回主内存。
为了提高性能,JVM和处理器会对此过程进行优化:尽量减少主内存和工作内存的数据交换次数。这就意味着两点:
- 当线程需要访问一个共享变量时,不会每次都从主内存中读取,而是访问工作内存中已有的这个变量。
- 当线程修改了共享变量的值时,可能不会立即写回主内存。
这时候就会存在一个问题,如下面的示例代码。
readerThread
可能会永远看不到flag
被设置为true
,因为它一直在自己的工作内存中查看flag
的旧值。
这种“线程对变量的修改对其他线程不可见”,就是可见性问题。
为了解决可见性问题,JMM提供了 volatile
关键字。它会告诉JVM和硬件这个变量是易变的,不应该被优化。意味着两点:
- 当线程需要访问一个
volatile
共享变量时,每次从主内存获取最新的值。 - 当线程修改了
volatile
变量的值,该变量的新值会立即被同步到主内存。
示例代码中的flag
在 volatile
修饰后,readerThread
会在flag
更新后去主内存获取最新的值,然后继续向下执行。
一致性问题
JMM还解释了在多线程环境下,由于内存访问的并发性和编译器、处理器优化的影响,会发生数据不一致的问题。如:
- 多线程在并发访问同一内存位置时,导致的数据不一致。
- 一些非原子操作,例如i++,在多线程的环境下的的数据不一致。
- CPU时间片轮转和指令重排序导致多线程环境下的数据不一致。
对于以上产生问题的几个场景,只要可以协调线程的执行顺序,确保结果是按照某种全局顺序执行的,就可以解决数据不一致的问题。
Java 开发人员都知道,通过同步机制可以保证这一点。
总结
虽然JAVA内存模型!=JVM内存模型,但两者也有一定的关联。
通过对JAVA内存模型的了解,能够对并发编程中存在的问题(可见性、原子性、有序性)有清晰的认识以及应对方案。