Skip to content

Java内存模型(JMM)

Java内存模型(Java Memory Model,简称JMM)是Java虚拟机规范中定义的一种抽象概念模型,它描述了程序中各个线程如何通过主内存和工作内存进行交互。

JMM的主要目标是解决由于多线程环境下的并发执行导致的可见性、原子性和有序性问题。

主要特点与概念:

  1. 主内存与工作内存

    • 主内存:所有变量都存储在主内存中,它是共享资源区域,被所有线程所见。
    • 工作内存:每个线程都有自己的工作内存,存储着从主内存读取的变量副本。线程对这些变量的操作(读写)都在工作内存中完成,然后根据需要将修改同步回主内存或从主内存刷新数据。
  2. 内存间交互操作

    • 写入(Store):将工作内存中的值写入到主内存中。
    • 读取(Load):从主内存中读取一个变量的值到工作内存。
    • 使用(Use):使用工作内存中的值。
    • 无效化(Invalidation):如果一个线程的工作内存中某个变量的值已经被更新,那么其他线程中该变量对应的值就会被标记为无效,需要重新从主内存加载。
    • 刷新(Flush):将工作内存中的所有变量值刷新回主内存。
  3. 内存一致性: JMM确保正确同步下,所有线程都能看到一致的数据视图。为了保证这一点,JMM定义了happens-before原则,用来指导程序员理解并推断程序执行时的内存可见性关系。

  4. 原子性: JMM要求对基本类型访问以及引用类型的赋值操作具有原子性,但复合操作(如i++)不是原子性的。

  5. 有序性: 由于编译器优化和处理器重排序的存在,可能导致程序实际执行顺序与代码逻辑顺序不一致。JMM通过禁止特定类型的重排序来保证特定场景下的有序性。

  6. 可见性: 确保当一个线程修改了变量的值后,其他线程能够看到这个变化。这通常通过synchronized、volatile关键字及锁机制等实现。

Java内存模型是一种规范,它定义了Java程序中各种并发操作的规则和保证,从而使得开发人员可以编写出正确的并发程序,同时避免出现数据竞争和其他并发问题。

抽象结构

image-20240212175837625

  1. 主内存:主内存是所有线程共享的数据存储区域,其中包含所有的共享变量。当一个线程修改了某个共享变量的值时,这个新值会从主内存复制到其他线程的本地内存中。

  2. 线程A和线程B:这两个代表了并发执行的两个线程。每个线程都有自己的本地内存区域,用于存储从主内存中读取的共享变量副本。

  3. 本地内存A和本地内存B:这是线程A和线程B各自的本地内存区域。这些本地内存区域包含了线程从主内存中读取的共享变量副本。请注意,本地内存并不是Java内存模型的一部分,它是一个抽象的概念,用来描述线程与主内存之间的交互行为

  4. JMM控制:Java内存模型通过一系列的规则来控制线程对共享变量的读写操作,以确保正确地同步线程之间的状态。这些规则包括可见性、有序性和原子性等。

  5. 共享变量:这些是被多个线程访问和修改的变量。当一个线程修改了某个共享变量的值时,其他线程需要能够看到这个新的值。

8种同步操作

image-20240212130315919

用于确保多线程环境下数据的一致性和正确同步的基础,其中read、load、use、assign、store、write六个操作构成了对普通变量的完整读写周期;lock和unlock则是为了保证对共享资源进行互斥访问时的正确同步。

主内存: lock、unlock

工作内存: read、load、use、assign、store、write

  1. lock: 主内存中的变量被锁定以进行写入操作,使得其他线程在该变量解锁之前不能对其进行读取或修改,即标识为线程独占状态

  2. unlock解除对主内存中变量的锁定状态,释放锁后其他线程可以获取该锁并进行更新操作。

  3. read: 从主内存读取一个变量的值到当前工作内存中,这个过程通常与后续的load操作一起确保数据可见性

  4. load: 将read得到的变量值存入工作内存中的本地副本,此时线程能够使用这个本地副本进行计算。

  5. use: 在工作内存中使用已经加载的变量值执行计算,即把工作内存中的一个变量值传递给执行引擎

  6. assign: 在工作内存中对变量赋新值,即把一个从执行引擎接收到的值赋值给工作内存的变量

  7. store: 将工作内存中的变量值刷新回主内存,但并不一定立即对所有线程可见。

  8. write: 与store操作类似,将更改后的值写入主内存中,配合内存屏障和volatile关键字等机制确保其他线程能观察到最新的值。

在实际应用中,通过synchronized、volatile关键字以及Java内存模型的内置规则,编译器和JVM会根据这些基本操作来实现线程间的内存交互和同步控制。

同步规则

可见性

当一个线程对主内存中的变量执行了unlock操作后,其他线程对该变量的lock操作能确保看到之前的所有修改。同时,readload操作组合起来保证从主内存读取到的数据是最新的(在volatile变量或synchronized块中尤其如此)。

当一个线程执行了storewrite操作,其他线程在完成对应的readload操作后,可以观察到这个线程对共享变量所做的更新。

不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中

有序性

JMM通过happens-before原则以及内存屏障来维护程序的执行顺序。

例如,对一个变量的unlock操作先行发生于随后对同一变量的lock操作;而storewrite操作先行发生于后续的readload操作。此外,volatile变量规则还规定对volatile变量的写操作先行发生于随后的读操作。

如果要把一个变量从主内存中复制到工作内存,就需要按顺序地执行read和load操作,同样,如果把变量从工作内存中同步回主内存中就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行

一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作

如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量

对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)

数据依赖性保护

JMM中的内存屏障可以防止编译器重排序和处理器乱序执行破坏数据依赖关系。

例如,在store-store、load-load、load-store、store-load四种类型的内存屏障中,有的禁止前向重排序,有的禁止后向重排序,从而确保多线程环境下的数据一致性。

如果对一个变量执行lock操作,将会清空工作内存中此变量的值在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值

不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中

不允许read和load、store和write操作之一单独出现

一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现

综上所述,JMM的同步规则是通过8种基本操作以及它们之间的有序性和内存屏障来实现的,这些机制共同作用以保证并发编程中的正确性,如可见性、有序性和数据依赖性的保持。