在 JVM (Java Virtual Machine) 中,内存的设计主要分为主内存和工作内存(又称为线程内存)。这种设计是基于 Java 内存模型(Java Memory Model, JMM) 的规定,它确保了多线程环境下数据的一致性和线程间的通信。
1. 主内存与工作内存的概念
1.1 主内存
- 主内存是所有线程共享的内存区域,主要存储程序中所有的实例对象和类变量。
- 它对应于 JVM 堆内存(Heap)和方法区(Method Area)。
- 主内存中的数据是线程共享的,因此线程之间通过主内存通信。
1.2 工作内存
- 工作内存是线程的私有区域,用于存储线程从主内存中拷贝的数据的副本。
- 它对应于线程栈(Thread Stack),存放线程独立的局部变量、操作栈和部分对象引用。
- 每个线程只能访问自己的工作内存,不能直接操作其他线程的工作内存。
2. 主内存与工作内存的关系
主内存和工作内存的关系类似于共享内存与高速缓存之间的关系:
-
线程对变量的所有操作(读取、写入)都必须先在工作内存中进行。
- 线程从主内存将变量值读取到工作内存中。
- 对变量的修改也会先在工作内存中完成,然后再同步回主内存。
-
线程不能直接操作主内存中的变量,所有变量必须通过工作内存中转。
示意图
主内存 (共享变量)
↑ ↓
工作内存 (线程1)
↑ ↓
工作内存 (线程2)
3. 主内存与工作内存的交互
JMM 定义了一组原子操作来完成主内存与工作内存之间的交互:
操作 | 描述 |
---|---|
lock | 把主内存中的变量标记为线程独占状态。 |
unlock | 解除对主内存变量的独占状态,释放给其他线程使用。 |
read | 从主内存中读取变量值到工作内存。 |
load | 把工作内存中的变量加载到线程的工作内存中。 |
use | 把工作内存中变量的值传递给执行引擎。 |
assign | 把执行引擎的值赋值给工作内存中的变量。 |
store | 把工作内存中的变量值写回主内存。 |
write | 把主内存的变量值更新为工作内存中的值。 |
交互流程
以变量 x
为例:
- 读取过程:线程从主内存中
read x
,然后load x
到工作内存。 - 操作过程:线程在工作内存中
use x
或assign x
进行计算。 - 写入过程:线程将工作内存中
store x
,然后write x
更新到主内存。
4. 主内存和工作内存的特点
4.1 主内存
- 线程共享:主内存是所有线程共享的,用于存储全局变量、类变量和堆上的对象。
- 数据一致性:主内存是线程间通信的桥梁,所有线程对共享变量的修改都必须最终同步到主内存。
4.2 工作内存
- 线程私有:工作内存是每个线程独立的,用于存储线程从主内存中拷贝的变量值。
- 临时存储:工作内存中的变量值只是主内存的一个副本,线程操作完成后需要同步回主内存。
- 提高效率:减少线程对主内存的频繁访问。
5. 主内存与工作内存的典型问题
5.1 可见性问题
- 如果一个线程修改了变量的值,但没有及时刷新到主内存,其他线程无法感知到最新的变量值。
- 示例:
public class VisibilityExample { private static boolean flag = true; public static void main(String[] args) { new Thread(() -> { while (flag) { // 如果flag没有及时刷新到主内存,该线程可能无法退出循环 } }).start(); new Thread(() -> { flag = false; // 修改flag值,但未及时刷新到主内存 }).start(); } }
5.2 指令重排序问题
- JVM 或 CPU 可能会对代码的执行顺序进行优化,导致线程看到的操作顺序与程序代码不一致。
- 示例:
public class ReorderingExample { private static boolean flag = false; private static int value = 0; public static void main(String[] args) { new Thread(() -> { value = 42; // 可能先执行 flag = true; }).start(); new Thread(() -> { if (flag) { System.out.println(value); // 可能输出 0 而不是 42 } }).start(); } }
5.3 解决方法
- 使用
volatile
:- 保证变量的可见性和禁止指令重排序。
- 使用同步机制:
- 通过
synchronized
或锁机制来确保线程间的同步。
- 通过
6. 主内存与 JVM 内存结构的关系
主内存主要对应于以下 JVM 内存区域:
- 堆内存(Heap):
- 存储对象实例,所有线程共享。
- 方法区(Method Area):
- 存储类信息、常量池和静态变量,所有线程共享。
工作内存主要对应于以下 JVM 内存区域:
- 线程栈(Thread Stack):
- 存储局部变量和操作栈,每个线程独立。
- 程序计数器(Program Counter):
- 跟踪当前线程执行到的字节码指令,每个线程独立。
7. 实际应用中的主内存与工作内存
7.1 可见性问题与 volatile
volatile
关键字确保变量的修改对所有线程可见,防止工作内存中的值与主内存不一致。
示例:
public class VolatileExample {
private static volatile boolean flag = true;
public static void main(String[] args) {
new Thread(() -> {
while (flag) {
// 保证线程可以感知flag的最新值
}
}).start();
new Thread(() -> {
flag = false; // 修改flag值
}).start();
}
}
7.2 同步问题与 synchronized
synchronized
确保线程对共享资源的访问是互斥的,且修改后的变量会立即同步到主内存。
示例:
public class SynchronizedExample {
private static int counter = 0;
public static synchronized void increment() {
counter++;
}
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) increment();
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Counter: " + counter); // 输出: 2000
}
}
8. 总结
- 主内存:存储共享数据,所有线程可访问。
- 工作内存:线程私有,存储主内存变量的副本。
- 典型问题:可见性问题、指令重排序、竞态条件。
- 解决方法:使用
volatile
保证可见性,使用synchronized
保证原子性和可见性。
这种主内存-工作内存的模型是 Java 内存模型的核心,帮助开发者在多线程环境下编写安全的并发程序。