Java 内存模型
JMM的抽象模型 - 堆与栈
Java内存模型(JMM)首先从一个逻辑的、抽象的视角定义了线程和主内存之间的关系。它规定了虚拟机在多线程环境下如何以及何时可以看到由其他线程修改过的共享变量的值,以及在必要时如何同步对共享变量的访问。
1. 线程栈 (Thread Stack)
私有性: 每个线程都有其自己独立的线程栈,线程之间无法直接访问对方的栈。存储内容:局部变量: 方法内部定义的所有局部变量。基本数据类型:int,long,double等基本类型的值直接存储在栈上。对象引用: 对象的引用地址(reference)存储在栈上,但对象实例本身存放在堆中。
2. 堆 (Heap)
共享性: 堆是所有线程共享的一块内存区域。存储内容:对象实例: 几乎所有的对象实例(new Object())都在堆上分配内存。成员变量: 对象的所有成员变量(包括基本类型和引用类型)都随对象一起存储在堆中。静态变量: 类的静态变量也存放在堆中(JDK 8及以后)。数组: 数组对象本身存放在堆中。
线程栈与堆的交互
线程通过存储在自己线程栈上的对象引用,来操作位于共享堆区中的对象实例及其成员变量。如果多个线程的栈中都持有对同一个堆对象的引用,它们就可以并发地访问和修改这个对象的成员变量。
JMM与硬件内存的映射
JMM是一个抽象概念,它需要映射到底层的物理硬件内存架构上才能被理解。
1. 硬件内存架构
现代计算机通常包含多核CPU。其内存架构大致如下:
* CPU: 每个CPU核心都有自己的寄存器(Registers)和高速缓存(Cache)。
* 主内存 (RAM): 所有CPU共享的物理内存。
为了提升性能,CPU并不会直接读写主内存,而是会先将主内存中的数据读取到自己的高速缓存中进行操作。操作完成后,再在某个时机将缓存中的数据写回到主内存。
2. JMM与硬件的连接
- JMM中定义的
主内存主要对应物理上的主内存 (RAM)。 - JMM中定义的
工作内存(一个更抽象的概念)可以理解为对应物理上的CPU高速缓存和寄存器。
当线程操作共享变量时,它实际上是将变量从主内存拷贝一份到自己的工作内存(CPU缓存)中,操作完成后再写回主内存。正是这种“拷贝-操作-写回”的机制,导致了并发编程中的两大核心问题。
并发编程的核心问题
1. 可见性 (Visibility)
问题描述: 当一个线程修改了共享变量的值,但还未将其写回主内存时,这个变更对其他线程是不可见的。其他线程仍然可能从主内存或自己的缓存中读取到旧的值。解决方案: 使用volatile关键字。volatile可以保证:- 对该变量的修改会立即被刷新到主内存。
- 其他线程在读取该变量时,会强制从主内存中重新获取最新值,而不是使用本地缓存。
2. 竞态条件 (Race Condition) 与原子性
问题描述: 多个线程同时对一个共享变量执行“读-改-写”操作序列时,由于这个序列不是原子性的,可能会导致计算结果不正确,产生竞态条件。例如,两个线程同时对count = 0执行count++,理想结果是2,但实际结果可能是1。解决方案: 使用synchronized块或锁(Lock)。synchronized可以保证:原子性: 确保在同一时刻,只有一个线程能进入被其修饰的代码块(临界区),从而保证操作的原子性。可见性: 当一个线程释放锁时,它会强制将工作内存中的修改刷新到主内存。而当另一个线程获取锁时,会清空工作内存,强制从主内存加载最新值。
JMM与JVM内存结构的区别
这是一个非常关键且容易混淆的概念:
JVM内存结构 (JVM Memory Structure): 也称为“JVM运行时数据区”。它描述的是JVM在执行Java程序时,从操作系统申请的内存被划分成了哪些区域,以及每个区域的功能和用途。它主要包括:堆、虚拟机栈、方法区、程序计数器、本地方法栈。它关注的是内存的划分和存储。Java内存模型 (JMM - Java Memory Model): 它是一个更抽象的概念,并非实际的内存布局。它定义了多线程环境下共享变量的访问规则和一致性保证,旨在屏蔽不同硬件和操作系统内存模型的差异,为Java程序员提供一个统一的、可预测的并发编程模型。它关注的是并发访问的规则和可见性。
简单来说,JVM内存结构是“有什么”,而JMM是“怎么办”。JMM是建立在JVM内存结构之上的并发访问规范。