Java内存模型介绍
JMM的核心目标是定义程序中各个变量的访问规则,特别是解决在多线程环境下,由于CPU高速缓存、编译器优化和指令重排序等底层机制,所导致的可见性和有序性问题。
简单来说,如果没有JMM,Java程序在多核处理器上运行时,一个线程对共享变量的修改,对于其他线程来说可能是不可见的,或者执行顺序是混乱的,从而导致各种并发错误。
JMM的核心抽象:主内存与工作内存
为了定义这套规则,JMM提出了一个抽象的模型,将内存划分为两个逻辑部分:
-
主内存 (Main Memory): 这是一个所有线程共享的区域,可以近似地对应于物理内存中的Java堆。 Java中所有的实例变量、静态变量都存储在主内存中。
-
工作内存 (Working Memory): 这是每个线程私有的区域,可以近似地对应于CPU的高速缓存或寄存器。 线程对变量的所有操作(读取、赋值等)都必须在自己的工作内存中进行,而不能直接读写主内存。 不同线程之间也无法直接访问对方的工作内存,线程间的通信必须通过主内存来完成。
一个线程修改共享变量的完整流程如下: 1. Read: 从主内存中读取变量的值。 2. Load: 将读取到的值加载到工作内存的变量副本中。 3. Use: 线程在工作内存中使用这个变量副本。 4. Assign: 将计算后的新值赋给工作内存中的变量副本。 5. Store: 将工作内存中变量副本的新值,存储到主内存中。 6. Write: 将存储到主内存的值,更新到主内存的变量中。
JMM正是通过定义这一系列操作的原子性、可见性和有序性规则,来保证并发的正确性。
JMM提供的并发保证与工具
JMM为我们开发者提供了几个关键的“武器”,来确保我们编写的并发程序能够正确地处理可见性和有序性问题。这些保证,我们称之为Happens-Before原则。
Happens-Before原则是JMM的核心,它定义了在多线程环境中,两个操作之间存在的偏序关系。如果操作A happens-before 操作B,那么A操作的结果对B操作是可见的,并且A操作的执行顺序在B操作之前。
JMM提供了以下几种方式来建立Happens-Before关系:
-
程序次序规则 (Program Order Rule): 在一个线程内,按照代码的先后顺序,前面的操作happens-before后面的操作。这是单线程内有序性的基本保证。
-
管程锁定规则 (Monitor Lock Rule): 一个unlock操作 happens-before 后续对同一个锁的lock操作。这就是synchronized关键字的魔力所在。当一个线程释放锁时,它会强制将自己工作内存中的修改刷新到主内存;当另一个线程获取锁时,它会清空自己的工作内存,强制从主内存加载最新的值。这同时保证了原子性和可见性。
-
volatile变量规则 (Volatile Variable Rule): 对一个volatile变量的写操作 happens-before 后续对这个变量的读操作。volatile通过内存屏障,实现了两点: 保证可见性:写操作会立即将新值刷新到主内存,并使其他线程的缓存失效。 禁止指令重排序:保证了代码的有序性。
-
线程启动规则 (Thread Start Rule): Thread对象的start()方法 happens-before 此线程的任何一个动作。
-
线程终止规则 (Thread Termination Rule): 线程中的所有操作都 happens-before 对此线程的终止检测(如thread.join()或thread.isAlive()的返回)。
-
线程中断规则 (Thread Interruption Rule): 对线程interrupt()方法的调用 happens-before 被中断线程的代码检测到中断事件的发生。
-
传递性 (Transitivity): 如果A happens-before B,且B happens-before C,那么A happens-before C。
总结
总而言之,JMM(Java内存模型)并不是一个物理存在,而是一套深刻影响我们如何编写并发程序的规则集。 * 它通过主内存和工作内存的抽象,解释了多线程并发问题的根源。 * 它通过Happens-Before原则,为我们提供了清晰的、跨平台的内存可见性保证。 * 它最终通过像synchronized、volatile这些关键字,让我们开发者能够在需要的时候,精确地控制线程间的同步,写出正确、可靠的并发程序。