Java内存模型JMM深入解析

 更新时间:2025年12月17日 11:57:12   作者:rchmin  
JMM(Java内存模型)规范了多线程环境下Java程序中变量的内存访问规则,解决了可见性、原子性和有序性问题,通过关键字和规则确保多线程程序的正确性和线程安全性,本文介绍Java内存模型JMM的相关知识,感兴趣的朋友一起看看吧

1. 什么是 JMM?

JMM 的全称是 Java Memory Model,即 Java 内存模型

简单来说,JMM 是一套规范,它定义了在多线程环境下,Java 程序中的变量(特别是共享变量)如何被写入内存以及如何从内存中读取的规则。

关键点:

  • 它不是 指 Java 程序运行时内存区域的划分(如堆、栈、方法区)。那是 JVM 内存结构,是两个不同的概念。
  • 它是 一个抽象的概念,是一组规则和规范,旨在解决由于多线程访问共享数据而可能引发的各种问题,如内存可见性、原子性、有序性等。

2. 为什么需要 JMM?(JMM 要解决的问题)

在没有 JMM 约束的情况下,多线程编程会面临三大核心难题,这主要是由于现代计算机架构(如多级缓存、CPU 指令重排序)造成的。

1. 可见性

  • 问题: 一个线程修改了共享变量的值,另一个线程不能立即看到这个修改。
  • 原因: 为了提高效率,每个线程都有自己的工作内存(可以理解为CPU高速缓存的一个抽象),它们会先将主内存中的共享变量拷贝一份到自己的工作内存中进行操作,操作完成后并不一定会立即写回主内存。如果线程A修改了值但未刷新到主内存,线程B读取到的就还是旧的值。
  • 例子:
// 共享变量
private static boolean flag = false;
public static void main(String[] args) {
    new Thread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true; // 线程A修改flag为true
        System.out.println("Flag set to true.");
    }).start();
    new Thread(() -> {
        while (!flag) { 
            // 线程B可能永远无法跳出循环,因为它看不到线程A对flag的修改
        }
        System.out.println("Thread sees flag change.");
    }).start();
}

在没有同步措施的情况下,第二个线程可能会陷入死循环。

2. 原子性

  • 问题: 一个或多个操作,要么全部执行成功,要么全部不执行,中间不能被任何其他操作中断。
  • 原因: 即使是看似简单的操作(如 i++),在底层也是由多个指令组成的(读取 i,计算 i+1,写回 i)。如果多个线程同时执行 i++,就可能发生线程A刚读取完 i 的值,CPU时间片就被线程B抢走,线程B也读取了相同的值并完成写入,然后线程A再继续写回,最终导致两次 i++ 结果只增加了1。
  • 例子: count++ 就不是原子操作。

3. 有序性

  • 问题: 程序执行的顺序不一定就是代码编写的顺序。
  • 原因: 为了性能优化,编译器和处理器常常会对指令进行重排序。只要在单线程环境下,重排序后的结果与顺序执行的结果一致(遵守 as-if-serial 语义),这种优化就是被允许的。但在多线程环境下,重排序可能会导致意想不到的结果。
  • 例子(经典的双重检查锁定单例模式问题):
public class Singleton {
    private static Singleton instance; // 没有volatile
    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton(); // 非原子操作,可能发生重排序
                }
            }
        }
        return instance;
    }
}

instance = new Singleton() 这行代码在 JVM 中大致做了三件事:

  • 分配对象的内存空间
  • 初始化对象
  • 将 instance 引用指向这块内存
  • 如果步骤2和3被重排序,线程A可能刚执行完步骤3(instance 已不为null)但还未初始化对象时,线程B在第一次检查 if (instance == null) 时发现不为null,就会直接返回一个尚未初始化完成的错误对象。

3. JMM 是如何解决这些问题的?

JMM 通过定义一些关键的 关键字 和 规则 来解决上述问题,主要是围绕 主内存 和 工作内存 之间的交互协议。

核心手段:

  • synchronized 关键字
    • 原子性: synchronized 块中的操作具有原子性,同一时刻只有一个线程能执行。
    • 可见性: 当线程进入 synchronized 块时,会清空工作内存,从主内存重新加载变量。退出 synchronized 块时,会把工作内存中的修改刷新到主内存。
    • 有序性: 它通过“一个变量在同一时刻只允许一条线程对其进行 lock 操作”来限制重排序,从而保证有序性。可以看作是单线程执行。
  • volatile 关键字
    • 可见性: 当写一个 volatile 变量时,JMM 会立即将该线程工作内存中的新值强制刷新到主内存。当读一个 volatile 变量时,JMM 会使该线程的工作内存无效,从而从主内存中重新读取。
    • 有序性: 它通过插入内存屏障 来禁止指令重排序。确保了 volatile 写操作之前的任何读写操作都不会被重排序到写操作之后;volatile 读操作之后的任何读写操作都不会被重排序到读操作之前。
    • 注意: volatile 不保证原子性(例如 volatile int i; i++ 仍然不是原子的)。
  • Happens-Before 原则
    • 这是 JMM 中最核心、最复杂的概念之一。它是一组规则,用于描述两个操作之间的内存可见性。如果操作 A Happens-Before 于操作 B,那么 A 操作所做的任何修改对 B 操作都是可见的。
  • 程序次序规则: 在一个线程内,书写在前面的操作先行发生于书写在后面的操作。
  • 管程锁定规则: 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
  • volatile变量规则: 对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
  • 线程启动规则: Thread 对象的 start() 方法先行发生于此线程的每一个动作。
  • 线程终止规则: 线程中的所有操作都先行发生于对此线程的终止检测。
  • 线程中断规则: 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  • 对象终结规则: 一个对象的初始化完成先行发生于它的 finalize() 方法的开始。
  • 传递性: 如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。

总结

特性问题描述JMM 解决方案
原子性操作被中途打断synchronized
可见性一个线程的修改对其他线程不可见synchronizedvolatile, Happens-Before
有序性指令执行顺序与代码顺序不一致synchronizedvolatile, Happens-Before

一句话总结:
JMM(Java内存模型)是一套规范,它屏蔽了底层硬件内存访问的差异,为 Java 开发者提供了一套统一的内存访问模型,使得我们在编写多线程程序时,即使在不了解底层硬件细节的情况下,也能通过使用 synchronizedvolatile 等关键字,编写出正确、线程安全的代码。

到此这篇关于Java内存模型JMM深入解析的文章就介绍到这了,更多相关Java内存模型JMM内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Nacos配置中心与本地代码工程配置文件之间的优先级关系详解

    Nacos配置中心与本地代码工程配置文件之间的优先级关系详解

    本文介绍了Spring Cloud生态中配置加载原理,强调Nacos远程配置优先级高于本地`application.yml`但低于命令行和环境变量,覆盖了多环境配置、动态刷新配置及安全配置外置等应用场景,对比了Nacos配置中心与本地配置文件的优缺点,并给出最佳实践建议
    2026-04-04
  • Java中Spring获取bean方法小结

    Java中Spring获取bean方法小结

    Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架,如何在程序中获取Spring配置的bean呢?下面通过本文给大家介绍Java中Spring获取bean方法小结,对spring获取bean方法相关知识感兴趣的朋友一起学习吧
    2016-01-01
  • 剑指Offer之Java算法习题精讲数组与列表的查找及字符串转换

    剑指Offer之Java算法习题精讲数组与列表的查找及字符串转换

    跟着思路走,之后从简单题入手,反复去看,做过之后可能会忘记,之后再做一次,记不住就反复做,反复寻求思路和规律,慢慢积累就会发现质的变化
    2022-03-03
  • Java日常练习题,每天进步一点点(59)

    Java日常练习题,每天进步一点点(59)

    下面小编就为大家带来一篇Java基础的几道练习题(分享)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧,希望可以帮到你
    2021-08-08
  • Java实现压缩图片大小

    Java实现压缩图片大小

    这篇文章主要为大家详细介绍了Java实现压缩图片大小,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-04-04
  • 解读Spring-boot的debug调试

    解读Spring-boot的debug调试

    这篇文章主要介绍了解读Spring-boot的debug调试,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-12-12
  • SpringMVC 通过ajax 实现文件上传的步骤

    SpringMVC 通过ajax 实现文件上传的步骤

    使用form表单在springmvc 项目中上传文件,文件上传成功之后往往会跳转到其他的页面,但是有的时候,文件上传成功的同时,并不需要进行页面的跳转,可以通过ajax来实现文件的上传,下面给大家介绍SpringMVC 通过ajax 实现文件上传的步骤,感兴趣的朋友一起看看吧
    2025-05-05
  • 学习不同 Java.net 语言中类似的函数结构

    学习不同 Java.net 语言中类似的函数结构

    这篇文章主要介绍了学习不同 Java.net 语言中类似的函数结构,函数式编程语言包含多个系列的常见函数。但开发人员有时很难在语言之间进行切换,因为熟悉的函数具有不熟悉的名称。函数式语言倾向于基于函数范例来命名这些常见函数。,需要的朋友可以参考下
    2019-06-06
  • spring整合kaptcha验证码的实现

    spring整合kaptcha验证码的实现

    这篇文章主要介绍了spring整合kaptcha验证码的实现,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-05-05
  • SpringBoot Redis批量存取数据的操作

    SpringBoot Redis批量存取数据的操作

    这篇文章主要介绍了SpringBoot Redis批量存取数据的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08

最新评论