Java内存模型JMM和内存区域

Java内存模型

Java内存模型(Java Memory Model, JMM)是Java虚拟机(JVM)规范中定义的一种抽象模型,用于描述多线程环境下,线程如何与内存交互以及如何保证线程之间的可见性、有序性和原子性。JMM是理解Java并发编程的基础。


1. Java内存模型的核心概念

Java内存模型定义了以下核心概念:

(1)主内存(Main Memory)

  • 主内存是所有线程共享的内存区域,存储了所有的变量(实例字段、静态字段等)。
  • 主内存是线程之间通信的桥梁。

(2)工作内存(Working Memory)

  • 每个线程都有自己的工作内存,存储了该线程使用的变量的副本。
  • 线程对变量的所有操作(读取、赋值等)都发生在工作内存中,不能直接操作主内存。

(3)内存间的交互

  • 线程不能直接操作主内存中的变量,而是需要通过以下操作与主内存交互:
    • 读取(Read):从主内存中读取变量的值到工作内存。
    • 加载(Load):将读取的值放入工作内存的变量副本中。
    • 使用(Use):线程使用工作内存中的变量值。
    • 赋值(Assign):线程将新值赋给工作内存中的变量。
    • 存储(Store):将工作内存中的变量值写回主内存。
    • 写入(Write):将存储的值更新到主内存中的变量。

2. Java内存模型的三大特性

JMM通过以下三大特性来保证多线程程序的正确性:

(1)原子性(Atomicity)

  • 原子性是指一个操作是不可分割的,要么全部执行成功,要么全部不执行。
  • 示例
    • 基本数据类型的读写操作是原子的(如intboolean等)。
    • longdouble的读写操作在32位JVM中可能不是原子的。
  • 如何保证原子性
    • 使用synchronized关键字。
    • 使用java.util.concurrent.atomic包中的原子类(如AtomicInteger)。

(2)可见性(Visibility)

  • 可见性是指一个线程对共享变量的修改能够及时被其他线程看到。
  • 问题
    • 由于线程的工作内存是独立的,一个线程修改了共享变量后,其他线程可能无法立即看到修改。
  • 如何保证可见性
    • 使用volatile关键字。
    • 使用synchronized关键字。
    • 使用final关键字(确保变量在构造完成后可见)。

(3)有序性(Ordering)

  • 有序性是指程序执行的顺序按照代码的先后顺序执行。
  • 问题
    • 由于指令重排序(编译器、处理器优化),代码的执行顺序可能与编写顺序不一致。
  • 如何保证有序性
    • 使用volatile关键字(禁止指令重排序)。
    • 使用synchronized关键字。
    • 使用happens-before规则。

3. happens-before规则

happens-before规则是JMM中定义的一组规则,用于描述多线程操作之间的可见性和有序性。如果操作A happens-before 操作B,那么操作A的结果对操作B可见。

happens-before规则包括:

  • 程序顺序规则:在一个线程中,前面的操作happens-before后面的操作。
  • volatile规则:对一个volatile变量的写操作happens-before后续对该变量的读操作。
  • 锁规则:解锁操作happens-before后续的加锁操作。
  • 线程启动规则:线程的start()方法happens-before该线程的任何操作。
  • 线程终止规则:线程的所有操作happens-before其他线程检测到该线程已经终止。
  • 传递性规则:如果A happens-before B,且B happens-before C,那么A happens-before C。

4. volatile关键字

volatile是JMM中用于保证可见性和有序性的关键字。

volatile的作用:

  • 保证可见性:对volatile变量的写操作会立即刷新到主内存,读操作会从主内存中读取最新值。
  • 禁止指令重排序:volatile变量的读写操作不会被重排序。

volatile的局限性:

  • volatile只能保证单个变量的原子性,不能保证复合操作的原子性(如i++)。

5. synchronized关键字

synchronized是JMM中用于保证原子性、可见性和有序性的关键字。

synchronized的作用:

  • 保证原子性:同一时刻只有一个线程可以执行synchronized代码块。
  • 保证可见性:线程在释放锁时会将工作内存中的变量刷新到主内存,获取锁时会从主内存中读取最新值。
  • 保证有序性:synchronized代码块内的操作不会被重排序。

6. final关键字

final关键字也可以用于保证可见性。

final的作用:

  • final修饰的字段在构造完成后,对其他线程可见。
  • 禁止指令重排序,确保对象构造完成后才能被其他线程访问。

7. Java内存模型的实现

JMM的具体实现依赖于JVM。不同的JVM实现(如HotSpot、OpenJ9等)可能会采用不同的优化策略,但都必须遵守JMM的规范。


总结

Java内存模型(JMM)是Java并发编程的核心,它通过定义主内存、工作内存以及内存间的交互规则,保证了多线程程序的原子性、可见性和有序性。理解JMM的关键在于掌握happens-before规则、volatilesynchronized的作用,以及如何通过这些机制编写正确的并发程序。

内存区域

  • 定义:JVM内存区域是JVM在运行时管理内存的具体实现,描述了Java程序运行时内存的物理划分。
  • 核心:JVM内存区域关注的是内存的物理分配和管理
  • 主要内容
    • 堆(Heap):存储对象实例和数组,是所有线程共享的内存区域。
    • 栈(Stack):每个线程私有的内存区域,存储局部变量、方法调用栈帧等。
    • 方法区(Method Area):存储类信息、常量、静态变量等,是所有线程共享的内存区域。
    • 程序计数器(PC Register):每个线程私有的内存区域,存储当前线程执行的指令地址。
    • 本地方法栈(Native Method Stack):为Native方法服务的内存区域。

JMM与JVM内存区域的关系

JMM和JVM内存区域是两个不同层次的概念,但它们之间有一定的联系:

(1)主内存与堆、方法区

  • JMM中的主内存可以理解为JVM内存区域中的方法区
    • 堆中存储的对象实例和数组是所有线程共享的,因此可以看作是主内存的一部分。
    • 方法区中存储的类信息、静态变量等也是所有线程共享的,因此也可以看作是主内存的一部分。

(2)工作内存与栈

  • JMM中的工作内存可以理解为JVM内存区域中的
    • 每个线程的栈中存储了局部变量、方法调用栈帧等,这些是线程私有的,因此可以看作是工作内存的一部分。

(3)内存交互与JVM内存区域

  • JMM中定义的内存交互操作(如Read、Load、Use、Assign、Store、Write)实际上是在JVM内存区域中进行的。
    • 例如,线程从主内存(堆或方法区)读取变量到工作内存(栈),或者将工作内存中的变量写回主内存。

4. JMM与JVM内存区域的侧重点

  • JMM
    • 关注的是多线程环境下,线程如何与内存交互以及如何保证线程之间的可见性、有序性和原子性。
    • 是一个抽象的概念,与具体的硬件和JVM实现无关。
  • JVM内存区域
    • 关注的是Java程序运行时内存的物理划分和管理。
    • 是JVM的具体实现,与硬件和JVM实现相关。

5. 示例

以下是一个简单的示例,说明JMM和JVM内存区域的关系:

public class Example {
    private static int sharedVar = 0; // 存储在方法区(主内存)
    private int instanceVar = 0;      // 存储在堆(主内存)

    public void method() {
        int localVar = 1; // 存储在栈(工作内存)
        sharedVar = localVar; // 从工作内存读取localVar,写入主内存的sharedVar
        instanceVar = localVar; // 从工作内存读取localVar,写入主内存的instanceVar
    }
}
  • JMM视角

    • sharedVarinstanceVar是主内存中的变量。
    • localVar是工作内存中的变量。
    • 线程在method()方法中对sharedVarinstanceVar的写操作需要遵循JMM的规则(如可见性、有序性)。
  • JVM内存区域视角

    • sharedVar存储在方法区。
    • instanceVar存储在堆中。
    • localVar存储在栈中。

总结

  • Java内存模型(JMM)是一个抽象的概念,定义了多线程环境下线程如何与内存交互,关注的是可见性、有序性和原子性。
  • JVM内存区域是JVM在运行时管理内存的具体实现,包括堆、栈、方法区等,关注的是内存的物理分配和管理。
  • JMM中的主内存对应JVM内存区域中的方法区工作内存对应
  • 理解JMM和JVM内存区域的关系,有助于更好地掌握Java并发编程和内存管理的知识。

同步机制理解补充

在没有同步机制(如volatilesynchronized等)的情况下,即使一个线程执行了类似sharedVar = localVar;这样的指令,其他线程也可能无法立即看到sharedVar的变化。这是因为Java内存模型(JMM)允许线程将共享变量的值缓存在自己的工作内存中,而不是每次都从主内存中读取或写入。


1. 为什么其他线程可能看不到修改?

  • 工作内存的独立性
    • 每个线程都有自己的工作内存,存储了主内存中共享变量的副本。
    • 线程对共享变量的操作(读取、赋值等)都是在其工作内存中进行的,而不是直接操作主内存。
  • 缓存一致性问题
    • 当一个线程修改了共享变量(如sharedVar),这个修改可能只会更新到该线程的工作内存中,而不会立即写回主内存。
    • 其他线程的工作内存中仍然保存着旧的变量值,因此无法看到最新的修改。

2. 示例代码分析

public class Example {
    private static int sharedVar = 0; // 共享变量,存储在方法区(主内存)

    public void update() {
        int localVar = 1; // 局部变量,存储在栈(工作内存)
        sharedVar = localVar; // 将localVar的值赋给sharedVar
    }

    public void print() {
        System.out.println(sharedVar); // 读取sharedVar的值
    }
}
  • 情景

    • 线程A调用update()方法,将sharedVar的值更新为1
    • 线程B调用print()方法,读取sharedVar的值。
  • 问题

    • 如果线程A对sharedVar的修改没有及时写回主内存,或者线程B没有从主内存中读取最新的值,那么线程B可能会输出0(旧值),而不是1(新值)。

3. 如何保证其他线程能看到修改?

为了确保一个线程对共享变量的修改对其他线程可见,需要使用同步机制。以下是几种常见的方式:

(1)使用volatile关键字

  • volatile可以保证变量的可见性。
  • 当一个线程修改了volatile变量时,新值会立即写回主内存。
  • 当其他线程读取volatile变量时,会从主内存中读取最新值。
private static volatile int sharedVar = 0; // 使用volatile修饰

(2)使用synchronized关键字

  • synchronized可以保证原子性、可见性和有序性。
  • 当一个线程释放锁时,会将工作内存中的变量写回主内存。
  • 当另一个线程获取锁时,会从主内存中读取最新值。
public synchronized void update() {
    int localVar = 1;
    sharedVar = localVar;
}

public synchronized void print() {
    System.out.println(sharedVar);
}

(3)使用java.util.concurrent工具类

  • AtomicIntegerReentrantLock等工具类也可以保证可见性和原子性。
private static AtomicInteger sharedVar = new AtomicInteger(0);

public void update() {
    sharedVar.set(1); // 原子更新
}

public void print() {
    System.out.println(sharedVar.get()); // 原子读取
}

4. happens-before规则

JMM通过happens-before规则来定义操作之间的可见性。如果操作A happens-before 操作B,那么操作A的结果对操作B可见。

  • 示例
    • 线程A在释放锁之前修改了sharedVar
    • 线程B在获取锁之后读取sharedVar
    • 根据happens-before规则,线程A的修改对线程B可见。

  • 在没有同步机制的情况下,一个线程对共享变量的修改可能不会立即对其他线程可见。
  • 为了保证可见性,可以使用volatilesynchronizedjava.util.concurrent工具类。
  • 理解JMM的可见性和happens-before规则,是编写正确并发程序的关键。

在你的例子中,如果sharedVar没有被volatile修饰,或者没有使用其他同步机制,那么其他线程可能无法立即看到sharedVar的变化。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容