位置: 文档库 > Java > 13 张图解 Java 中的内存模型

13 张图解 Java 中的内存模型

醉卧红尘 上传于 2024-04-19 02:32

《13 张图解 Java 中的内存模型》

Java 作为一门广泛使用的编程语言,其内存模型是理解程序运行机制的关键。Java 内存模型(Java Memory Model,JMM)定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问规则,确保在不同处理器架构和操作系统下,多线程程序能够正确、一致地运行。本文将通过 13 张精心绘制的图解,深入剖析 Java 中的内存模型。

一、计算机硬件内存架构基础

在探讨 Java 内存模型之前,我们需要先了解计算机硬件的内存架构。现代计算机通常由多个处理器(CPU)和主内存(Main Memory)组成,处理器与主内存之间通过总线进行数据传输。

图 1 展示了计算机硬件内存架构的基本模型。多个 CPU 核心共享主内存,每个 CPU 核心有自己的高速缓存(Cache),用于存储频繁访问的数据,以减少对主内存的访问次数,提高程序运行效率。

硬件内存架构图

由于高速缓存的存在,不同 CPU 核心访问同一变量时,可能会出现数据不一致的问题。例如,CPU1 修改了某个变量的值并写入自己的高速缓存,但 CPU2 仍然从主内存或其他高速缓存中读取该变量的旧值,这就导致了数据的不一致性。

二、Java 内存模型概述

Java 内存模型的主要目标是定义程序中各个变量的访问规则,使得在多线程环境下,变量的读取和写入操作具有明确的顺序和可见性。

图 2 展示了 Java 内存模型的基本结构。Java 内存模型将内存分为主内存和工作内存。主内存是所有线程共享的内存区域,存放了程序中所有的变量。工作内存是每个线程私有的内存区域,每个线程都有自己的工作内存,线程在工作内存中保存了被该线程使用到的变量的主内存副本。

Java内存模型结构图

线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

三、内存间的交互操作

Java 内存模型定义了 8 种操作来完成主内存和工作内存之间的交互,下面我们将详细介绍这些操作,并通过图解来辅助理解。

1. lock(锁定)

lock 操作作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

图 3 展示了 lock 操作的过程。当一个线程执行 lock 操作时,它会获取该变量的锁,使得其他线程无法对该变量进行操作。

lock操作图


// 示例代码:使用 synchronized 关键字实现 lock 操作的效果
public class LockExample {
    private int sharedVariable;
    public synchronized void modifyVariable() {
        sharedVariable++;
    }
}

在上述代码中,synchronized 关键字修饰的方法在执行时会对当前对象进行加锁,相当于执行了 lock 操作,保证了同一时间只有一个线程能够进入该方法修改 sharedVariable 变量。

2. unlock(解锁)

unlock 操作作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

图 4 展示了 unlock 操作的过程。当线程执行完对变量的操作后,执行 unlock 操作释放锁,其他线程就可以获取该锁并对变量进行操作。

unlock操作图


// 示例代码:synchronized 方法执行完毕后自动解锁
public class UnlockExample {
    private int sharedVariable;
    public synchronized void modifyVariable() {
        sharedVariable++;
        // 方法执行完毕后自动执行 unlock 操作
    }
}

3. read(读取)

read 操作作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 操作使用。

图 5 展示了 read 操作的过程。线程从主内存中读取变量的值到自己的工作内存中。

read操作图

4. load(载入)

load 操作作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。

图 6 展示了 load 操作的过程。read 操作读取的值通过 load 操作存入工作内存的变量副本中。

load操作图


// 示例代码:read 和 load 操作的隐式执行
public class ReadLoadExample {
    private int sharedVariable;
    public void readVariable() {
        int localVariable = sharedVariable; // 隐式执行了 read 和 load 操作
    }
}

在上述代码中,将 sharedVariable 赋值给 localVariable 时,系统会自动执行 read 操作从主内存读取 sharedVariable 的值,然后执行 load 操作将该值存入工作内存的临时变量中。

5. use(使用)

use 操作作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。

图 7 展示了 use 操作的过程。工作内存中的变量值通过 use 操作传递给执行引擎进行计算等操作。

use操作图

6. assign(赋值)

assign 操作作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。

图 8 展示了 assign 操作的过程。执行引擎计算得到的值通过 assign 操作赋给工作内存中的变量。

assign操作图


// 示例代码:use 和 assign 操作的隐式执行
public class UseAssignExample {
    private int sharedVariable;
    public void modifyVariable() {
        sharedVariable = 10; // 隐式执行了 assign 操作,执行引擎将 10 赋给工作内存中的 sharedVariable 副本
        int result = sharedVariable * 2; // 隐式执行了 use 操作,将工作内存中的 sharedVariable 值传递给执行引擎进行计算
    }
}

7. store(存储)

store 操作作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。

图 9 展示了 store 操作的过程。工作内存中的变量值通过 store 操作传送到主内存中。

store操作图

8. write(写入)

write 操作作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值写入主内存的变量中。

图 10 展示了 write 操作的过程。store 操作传送的值通过 write 操作写入主内存的变量中。

write操作图


// 示例代码:store 和 write 操作的隐式执行
public class StoreWriteExample {
    private int sharedVariable;
    public void updateVariable() {
        sharedVariable = 20; // 隐式执行了 assign、store 和 write 操作
    }
}

在上述代码中,将 20 赋给 sharedVariable 时,系统会先执行 assign 操作将 20 赋给工作内存中的 sharedVariable 副本,然后执行 store 操作将该值传送到主内存,最后执行 write 操作将值写入主内存的 sharedVariable 变量中。

四、内存模型的三大特性

Java 内存模型具有原子性、可见性和有序性三大特性,下面我们将结合图解详细介绍。

1. 原子性

原子性是指一个操作是不可中断的,要么全部执行成功,要么全部不执行。Java 内存模型保证了基本的读取和赋值操作是原子性的。

图 11 展示了原子性操作的示例。对于单个变量的读取和赋值操作,在执行过程中不会被其他线程干扰。

原子性操作图


// 示例代码:原子性操作
public class AtomicExample {
    private int atomicVariable;
    public void setAtomicVariable(int value) {
        atomicVariable = value; // 这是一个原子性操作
    }
    public int getAtomicVariable() {
        return atomicVariable; // 这也是一个原子性操作
    }
}

但对于多个变量的复合操作,如 i++,它实际上包含了读取、赋值和写入三个操作,不是原子性的。在多线程环境下,可能会出现线程安全问题。

2. 可见性

可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

图 12 展示了可见性问题。如果没有正确的同步机制,线程 A 修改了共享变量的值,但线程 B 可能仍然读取到旧值。

可见性问题图

Java 提供了 volatile 关键字、synchronized 关键字和 final 关键字来保证可见性。


// 示例代码:使用 volatile 关键字保证可见性
public class VisibilityExample {
    private volatile boolean flag = false;
    public void writer() {
        flag = true;
    }
    public void reader() {
        while (!flag) {
            // 等待 flag 变为 true
        }
    }
}

在上述代码中,volatile 修饰的 flag 变量保证了当一个线程修改了它的值后,其他线程能够立即看到这个修改。

3. 有序性

有序性是指程序执行的顺序按照代码的先后顺序执行。但在多线程环境下,由于编译器优化和处理器重排序等原因,可能会出现指令重排序的问题,导致程序执行顺序与代码顺序不一致。

图 13 展示了有序性问题。线程 A 和线程 B 的执行顺序可能会因为指令重排序而发生改变,导致程序出现意想不到的结果。

<a href=有序性问题图">

Java 提供了 volatile 关键字和 synchronized 关键字来保证有序性。volatile 关键字通过插入内存屏障来禁止指令重排序,synchronized 关键字则通过锁定和解锁的机制来保证代码块内的指令顺序执行。

五、总结

本文通过 13 张图解详细介绍了 Java 中的内存模型,包括计算机硬件内存架构基础、Java 内存模型概述、内存间的交互操作以及内存模型的三大特性。理解 Java 内存模型对于编写高效、正确的多线程程序至关重要。在实际开发中,我们要合理运用 volatile、synchronized 等关键字来保证程序的原子性、可见性和有序性,避免出现线程安全问题。

关键词:Java 内存模型、硬件内存架构、内存交互操作、原子性、可见性、有序性、volatile 关键字、synchronized 关键字

简介:本文通过 13 张图解深入剖析 Java 中的内存模型,包括计算机硬件内存架构基础、Java 内存模型概述、内存间的 8 种交互操作(lock、unlock、read、load、use、assign、store、write)以及内存模型的原子性、可见性和有序性三大特性,并介绍了 volatile 和 synchronized 关键字在保证内存模型特性方面的作用,帮助读者理解并运用 Java 内存模型编写正确的多线程程序。

Java相关