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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java8深入学习系列(一)lambda表达式介绍

    Java8深入学习系列(一)lambda表达式介绍

    Java8最值得学习的特性就是Lambda表达式和Stream API,所以我们学习java8的第一课就是学习lambda表达式,下面这篇文章主要给大家介绍了关于Java8学习之lambda的相关资料,文中介绍的非常详细,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-08-08
  • Android Home键监听的实现代码

    Android Home键监听的实现代码

    这篇文章主要介绍了Android Home 键监听的实现代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-12-12
  • SpringBoot整合Druid数据库连接池的方法

    SpringBoot整合Druid数据库连接池的方法

    Druid是Java语言中最好的数据库连接池。Druid能够提供强大的监控和扩展功能。这篇文章主要介绍了SpringBoot整合Druid数据库连接池的方法,需要的朋友可以参考下
    2020-07-07
  • Java语言----三种循环语句的区别介绍

    Java语言----三种循环语句的区别介绍

    下面小编就为大家带来一篇Java语言----三种循环语句的区别介绍。小编举得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-07-07
  • 详细解读java同步之synchronized解析

    详细解读java同步之synchronized解析

    synchronized关键字是Java里面最基本的同步手段,下面我们来一起学习一下
    2019-05-05
  • Spring Cloud 优雅下线以及灰度发布实现

    Spring Cloud 优雅下线以及灰度发布实现

    这篇文章主要介绍了Spring Cloud 优雅下线以及灰度发布实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • 关于ResultSet(结果集)用法详解

    关于ResultSet(结果集)用法详解

    本文将深入探讨 ResultSet 的基本概念、常见操作及使用注意事项,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-04-04
  • Java面向对象程序设计:继承,多态用法实例分析

    Java面向对象程序设计:继承,多态用法实例分析

    这篇文章主要介绍了Java面向对象程序设计:继承,多态用法,结合实例形式分析了java继承与多态的相关概念、原理、实现方法与操作注意事项,需要的朋友可以参考下
    2020-04-04
  • 深入浅出Java中重试机制的多种方式

    深入浅出Java中重试机制的多种方式

    重试机制在分布式系统中,或者调用外部接口中,都是十分重要的。重试机制可以保护系统减少因网络波动、依赖服务短暂性不可用带来的影响,让系统能更稳定的运行的一种保护机制。本文就来和大家聊聊Java中重试机制的多种方式
    2023-03-03
  • spring6+JDK17实现SSM起步配置文件

    spring6+JDK17实现SSM起步配置文件

    本文介绍了使用Spring6和JDK17配置SSM(Spring + Spring MVC + MyBatis)框架,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-01-01

最新评论