引言
在计算机系统的发展过程中, 由于 CPU 的运算速度和计算机存储速度之间巨大的差距. 为了解决 CPU 的运算速度和计算机存储速度之间巨大的差距, 设计人员在 CPU 和计算机存储之间加入了高速缓存来做为他们之间的桥梁, 在运算时, 先将数据拷贝到高速缓存中, 计算完成后再将结果写入计算机存储, 这样大大提高了计算效率, 避免重复多次访问计算机存储造成的 cpu 资源浪费.
尽管这样, CPU 还是存在很多空闲的时间段, 为了压榨 CPU 的性能, 多任务处理诞生了, 同时多任务处理导致任务之间共享资源的争抢, 从而引发了并发问题.
在 Java 应用程序中, 为了更好的解决并发问题, 就必须深入理解 Java 内存模型. Java 内存模型是 Java 虚拟机非常重要的一部分, 它用来指导 Java 虚拟机是如何与计算机硬件内存之间协同工作的. 在了解 Java 内存模型之前, 我们先来看看计算机硬件内存模型是怎么样的.
硬件内存模型
现代计算机通常有 2 个或以上的 CPU, 单个 CPU 可能有多个内核. 每个 CPU 内核中都包含一组寄存器, CPU 在寄存器中执行操作比在计算机主存储器中快的多.
同时每个 CPU 之上还存在高速缓存, 但高速缓存的层级和位置是不固定的, 现代计算机的缓存层级很多达到了三级, 未来可能更多. 缓存的位置也各有不同, 有的集成了部分缓存到 CPU 中. 同样, 缓存的读写速度也大大快于计算机主存储器, 在寄存器和主存储器之间, 这样的 CPU-- 缓存 -- 主存储的三层结构就构成了硬件内存模型.
CPU 在程序的执行过程中, 经常会频繁的调用相同的数据, 比如在一个循环内调用了位于另外一个物理地址的函数, 这个函数可能与当前指令的物理位置相距甚远, 因为程序使用的物理内存并不是连续的, 这就导致了需要花费很多不必要的时间在物理寻址上. 但如果在 CPU 计算之前会将所需要用到的数据先读到缓存中, 计算完成之后再一次性写入计算机主存储器, 就可以避免频繁访问计算机主存储器造成的资源浪费.
Java 内存模型
上面说了计算机的硬件内存模型, Java 内存模型和硬件内存模型有很多类似的地方. 由于存在不同的计算机操作系统类型和硬件类型, 导致各种平台下物理内存模型的不一致. 为了让 Java 上层开发有一个统一的内存访问操作, 保证多线程对共享数据的读写一致性, JVM 规范定义了 Java 内存模型(Java Memory Model JMM).
JMM 通过 happens-before 语义 (篇幅有限, 后面的文章再详细解说) 定义了 Java 对数据的统一访问规则. 这些数据主要包括实例字段, 静态字段和构成数组的元素, 但不包括局部变量, 方法参数和异常处理参数, 因为局部变量和方法参数是线程私有的, 不存在数据竞争问题.
引用类型比较特殊, 引用本身是线程私有的, 但它引用的对象是可被共享的.
JMM 还规定了所有的变量都存储在主内存中(Main Memory), 同时每个线程有自己的本地内存(Local Meory, 也叫工作内存), 本地内存中保存了所需要用到的主内存数据的拷贝. 线程对变量的读和写都在本地内存中进行.
是不是发现 JMM 和硬件内存模型存在很多相似之处? 主内存对应计算机主存储, 本地内存对应高速缓存. 但要知道它们虽然可以类比, 却并不是相同的东西.
本地内存仅仅是 JMM 的一个抽象概念, 实际上 JVM 中并不存在这样一个区域来对应, 这个区域在广义上可以包括缓存, 寄存器以及其他的硬件和编译器优化等等. 这句话可能听起来比较难懂, 我们只需要知道线程对共享变量的操作并不会直接访问主内存, 而是访问一个中间层, 这个中间层包含了主内存中变量的拷贝, 同时中间层的访问速度大大快于访问主内存的速度, 在一定的操作之后将结果统一写回主内存, 这样就大大提高了程序的性能.
同时也会产生另外一个问题, 同一个共享变量在每一个线程之中都会有一份拷贝(对引用类型, 并不是拷贝全部数据), 产生的线程越多, 缓存开销也就越大.
JVM 内存模型
JVM 内存模型定义的是线程堆栈和堆之间的内存划分, 它和 Java 内存模型是有区别的, 参照深入理解 Java 虚拟机中的解释:
这两者本没有关系. 如果一定要勉强对应, 那从变量, 主内存, 工作内存的定义来看, 主内存主要对应于 Java 堆中的对象实例数据部分, 而工作内存则对应于虚拟机栈中的部分区域. 从更低层次上说, 主内存就是物理内存, 而为了获取更好的执行速度, 虚拟机 (甚至是硬件系统本身的优化措施) 可能会让工作内存优先存储于寄存器和高速缓存中, 因为运行时主要访问 -- 读写的是工作内存.
所有的原始类型 (boolean,byte,short,char,int,long,float,double) 局部变量都存储在线程堆栈中, 不对其他线程共享. 堆中则包含了 Java 程序中创建的对象.
举个例子:
- public class MemoryModel {
- public int i = 0;
- public void methodOne() {
- int localVarOne = 1;
- SharedObject localVarTwo = SharedObject.sharedObject;
- Integer localVarThree = new Integer(1);
- }
- }
- public class SharedObject {
- pubic static SharedObject sharedObject = new SharedObject();
- public int sharedVarOne = 1;
- }
代码中局部变量 localVarOne 存储在线程堆栈中. 局部变量 localVarTwo 的引用存储在线程堆栈中, 但对象本身存储在堆上. 局部变量 localVarThree 同 localVarTwo 一样, 引用存储在线程堆栈中, 但对象本身存储在堆上. 不同的是多线程执行 methodOne 方法时, localVarTwo 由于是静态类型, 在堆中只有一份数据, 而 localVarThree 在堆和堆栈中都有多份数据. 局部变量对象的成员变量 sharedVarOne 也存储在堆上, 无论 sharedVarOne 是基本类型还是引用类型都是如此.
参考资料:
深入理解 Java 内存模型
深入理解 Java 虚拟机
Java 并发编程的艺术
http://tutorials.jenkov.com/java-concurrency/java-memory-model.html
来源: https://www.cnblogs.com/konck/p/9292584.html