news 2026/9/11 10:13:07

Java内存模型(JMM)详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 中大致做了三件事:

    1. 分配对象的内存空间

    2. 初始化对象

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

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

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

核心手段:

  1. synchronized关键字

    • 原子性:synchronized块中的操作具有原子性,同一时刻只有一个线程能执行。

    • 可见性:当线程进入synchronized块时,会清空工作内存,从主内存重新加载变量。退出synchronized块时,会把工作内存中的修改刷新到主内存。

    • 有序性:它通过“一个变量在同一时刻只允许一条线程对其进行 lock 操作”来限制重排序,从而保证有序性。可以看作是单线程执行。

  2. volatile关键字

    • 可见性:当写一个volatile变量时,JMM 会立即将该线程工作内存中的新值强制刷新到主内存。当读一个volatile变量时,JMM 会使该线程的工作内存无效,从而从主内存中重新读取。

    • 有序性:它通过插入内存屏障来禁止指令重排序。确保了volatile写操作之前的任何读写操作都不会被重排序到写操作之后;volatile读操作之后的任何读写操作都不会被重排序到读操作之前。

    • 注意:volatile不保证原子性(例如volatile int i; i++仍然不是原子的)。

  3. Happens-Before 原则
    这是 JMM 中最核心、最复杂的概念之一。它是一组规则,用于描述两个操作之间的内存可见性。如果操作 AHappens-Before于操作 B,那么 A 操作所做的任何修改对 B 操作都是可见的。

    • 程序次序规则:在一个线程内,书写在前面的操作先行发生于书写在后面的操作。

    • 管程锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。

    • volatile变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。

    • 线程启动规则:Thread 对象的start()方法先行发生于此线程的每一个动作。

    • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。

    • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。

    • 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。

    • 传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。

总结

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 8:08:00

3个颠覆性突破让开源CMS成为中小企业数字化转型的秘密武器

在数字化转型浪潮中,中小企业的IT预算往往捉襟见肘,而Directus作为一款完全开源的内容管理平台,正以零许可成本和高度灵活的技术架构,为预算有限的企业提供了一条全新的数字化路径。这款基于Node.js构建的现代化CMS,不…

作者头像 李华
网站建设 2026/9/9 10:35:27

PapersGPT for Zotero 终极安装指南:5步快速配置AI文献助手

PapersGPT for Zotero 终极安装指南:5步快速配置AI文献助手 【免费下载链接】papersgpt-for-zotero Zotero chat PDF with DeepSeek, GPT, ChatGPT, Claude, Gemini 项目地址: https://gitcode.com/gh_mirrors/pa/papersgpt-for-zotero PapersGPT for Zotero…

作者头像 李华
网站建设 2026/9/11 9:52:39

15-2.【Linux系统编程】进程信号 - 信号保存(信号处理流程的三种状态:未决、阻塞、递达,信号保存由未决表完成、sigset_t信号集类型及相关函数)

目录3. 保存信号-内核通过 “未决信号集” 为每个进程存储已产生但未处理的信号3.1 信号处理流程中的不同状态3.2 信号在内核中的表示3.3 sigset_t信号集类型3.4 信号集操作函数3.4.1 sigprocmask读取或更改进程的信号屏蔽字3.4.2 sigpending读取当前进程的未决信号集3.4.3 综合…

作者头像 李华
网站建设 2026/9/11 13:13:30

31、国际化文本输入方法详解

国际化文本输入方法详解 1. 字体集与字符显示 当 XFontSet 缺少字符集时,每个不可用的字符会使用 XCreateFontSet 返回的默认字符串来绘制。对于无效码点的行为则未作定义。 2. 输入方法概述 输入方法涵盖多个方面,包括输入方法概述、管理、功能、值、输入上下文功能与值…

作者头像 李华
网站建设 2026/9/10 14:45:56

33、本地化与国际化文本函数详解

本地化与国际化文本函数详解 在处理国际化文本输入时,有许多关键的概念和函数需要我们去理解和掌握。下面将详细介绍这些内容,包括输入方法值、输入上下文函数以及输入上下文值等方面。 输入方法相关函数与值 XUnregisterIMInstantiateCallback 函数:该函数用于移除之前…

作者头像 李华
网站建设 2026/9/10 10:59:37

40、资源管理器功能详解

资源管理器功能详解 1. 资源规范中的空白处理 在资源规范(ResourceSpec)里,名称或冒号前后的空白字符会被忽略。为了让值(Value)能以空白开头,“\space”(反斜杠加空格)会被识别并替换成空格字符,“\tab”(反斜杠加水平制表符)会被识别并替换成水平制表符。若要让…

作者头像 李华