当前位置: 首页 > article >正文

Java多线程与高并发专题——JMM

引入

上一篇我们讲到在并发场景中,存在结果问题和性能问题,其中结果问题主要是因为没有保障可见性、原子性、有序性等导致的。

这三者在编程领域属于共性问题,所有的编程语言都会遇到,Java在诞生之初就支持多线程,所以肯定有保障它们的技术方案。理解Java解决并发问题的方案,对于了解其他语言的解决方案有触类旁通的效果。

在Java中,对应的解决方案就是JMM(Java 内存模型)。

什么是 JMM?

为了更准确的理解什么是JMM,我们先来看一个很容易和它混淆的概念。

JVM 内存结构 VS JMM

JVM 内存结构和 JMM(Java 内存模型)是两个截然不同的概念,但是很容易搞混。

我们先从整体上概括一下这两者的主要作用:

  • JVM 内存结构和 Java 虚拟机的运行时区域有关;
  • Java 内存模型和 Java 的并发编程有关。

下面我们先看看JVM内存结构是怎么回事:

JVM 内存结构

我们知道,Java 代码是运行在JVM上的,而JVM在执行 Java 程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。

在《Java 虚拟机规范》(Java SE 8)中描述了 JVM 运行时内存区域结构可分为以下6 个区。

  • 堆区(Heap)
    堆是存储类实例和数组的,通常是内存中最大的一块。实例很好理解,比如 new Object() 就会生成一个实例;而数组也是保存在堆上面的,因为在Java 中,数组也是对象。
  • 虚拟机栈(Java Virtual Machine Stacks)
    它保存局部变量和部分结果,并在方法调用和返回中起作用。
  • 方法区(Method Area)
    它存储每个类的结构,例如运行时的常量池、字段和方法数据,以及方法和构造函数的代码,包括用于类初始化以及接口初始化的特殊方法。
  • 本地方法栈(Native Method Stacks)
    与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的 Java 方法服务,而本地方法栈则是为 Native 方法服务。
  • 程序计数器(The PC Register)
    是最小的一块内存区域,它的作用通常是保存当前正在执行的 JVM 指令地址。
  • 运行时常量池(Run-Time Constant Pool)
    是方法区的一部分,包含多种常量,范围从编译时已知的数字到必须在运行时解析的方法和字段引用。

注意:
以上的 JVM 规范,在不同的虚拟机实现上会各有不同。

JVM 内存结构是由 Java 虚拟机规范定义的,描述的是在 Java 程序执行过程中,由 JVM 管理的不同数据区域,各个区域有其特定的功能。

看完了 JVM 内存结构,就让我们继续来看 JMM。

JMM

我们知道导致可见性的原因是缓存,导致有序性的原因是编译优化,导致原子性的原因是线程切换

而保障它们的最直接的办法就是禁用这些优化方案,但是这么做就是纯纯的削足适履了。

正确的做法是要因地制宜,对于什么时候禁用它们,最好的就是按照开发者的要求来禁用。所以,为了解决这些问题,只需要提供给程序员按需禁用的方法即可。

而JMM与处理器、缓存、并发、编译器有关,它解决了 CPU 多级缓存、处理器优化、指令重排等导致的结果不可预期的问题。其本质就是针对多线程这些问题的一组规范,需要各个 JVM 遵守 JMM 规范去实现,来让开发者可以利用这些规范,确保自己开发的多线程程序,即便在不同的虚拟机上运行,得到的结果是一致且符合预期的。

针对 JMM,站在我们这些开发者的视角,其实就可以理解为,JMM 规范了 JVM 如何提供按需禁用缓存、编译优化和线程切换的方法。

具体来说,这些方法主要包括,Happens-Before 规则,以及我们多线程开发时经常会使用到的各种同步工具和关键字,包括 volatile、synchronized、Lock 等。

拓展

从 Java 代码到 CPU 指令

我们都知道,编写的Java 代码,最终还是要转化为 CPU 指令才能执行的。为了理解 Java 内存模型的作用,我们首先就来回顾一下从 Java 代码到最终执行的 CPU 指令的大致流程:

  • 最开始,我们编写的 Java 代码,是 *.java 文件;
  • 在编译(包含词法分析、语义分析等步骤)后,在刚才的 *.java 文件之外,会多出一个新的 Java 字节码文件(*.class);
  • JVM 会分析刚才生成的字节码文件(*.class),并根据平台等因素,把字节码文件转化为具体平台上的机器指令;
  • 机器指令则可以直接在 CPU 上运行,也就是最终的程序执行。

为什么需要 JMM(Java Memory Model,Java 内存模型)

在更早期的语言中,其实是不存在内存模型的概念的。

所以程序最终执行的效果会依赖于具体的处理器,而不同的处理器的规则又不一样,不同的处理器之间可能差异很大,因此同样的一段代码,可能在处理器 A 上运行正常,而在处理器 B 上运行的结果却不一致。同理,在没有 JMM 之前,不同的 JVM的实现,也会带来不一样的“翻译”结果。

所以 Java 非常需要一个标准,来让 Java 开发者、编译器工程师和 JVM 工程师能够达成一致。达成一致后,我们就可以很清楚的知道什么样的代码最终可以达到什么样的运行效果,让多线程运行结果可以预期,这个标准就是 JMM,这就是需要 JMM的原因。

如果不加以规范,那么同样的 Java 代码,完全可能产生不一样的执行效果,那是不可接受的,这也违背了 Java “书写一次、到处运行”的特点。

总结

JMM 主要关注的是线程之间的内存可见性、原子性和有序性等问题,它定义了线程和主内存之间的抽象关系,以及多线程环境下共享变量的访问规则,目的是为了让 Java 程序在不同的硬件和操作系统上都能有一致的结果。

后面我们会一一介绍,我们在多线程与高并发开发的时候,是如何保障原子性、可见性和有序性的。

 


http://www.kler.cn/a/524346.html

相关文章:

  • 生成模型:扩散模型(DDPM, DDIM, 条件生成)
  • 2501,20个窗口常用操作
  • 2025年美赛B题-结合Logistic阻滞增长模型和SIR传染病模型研究旅游可持续性-成品论文
  • C语言练习(29)
  • gesp(C++六级)(6)洛谷:P10109:[GESP202312 六级] 工作沟通
  • Flutter_学习记录_基本组件的使用记录
  • 实验作业管理系统的设计与实现
  • Leetcode刷题-不定长滑动窗口
  • Vue 组件开发:构建高效可复用的前端界面要素
  • Spark Streaming的背压机制的原理与实现代码及分析
  • 力扣面试150 快乐数 循环链表找环 链表抽象 哈希
  • Java中实现ECDSA算法介绍、应用场景和示例代码
  • 机器人介绍
  • 《HelloGitHub》第 106 期
  • 扣子平台音频功能:让声音也能“智能”起来。扣子免费系列教程(14)
  • 【Linux权限】—— 于虚拟殿堂,轻拨密钥启华章
  • YOLOv11-ultralytics-8.3.67部分代码阅读笔记-head.py
  • 基于SpringBoot的假期周边游平台的设计与实现(源码+SQL脚本+LW+部署讲解等)
  • JAVA实战开源项目:企业客户管理系统(Vue+SpringBoot) 附源码
  • C++并发编程指南04
  • 传统机器学习和深度学习
  • 29. C语言 可变参数详解
  • 谷堆论证引发的商业思考
  • 虚幻基础11:坐标计算旋转计算
  • 基于Docker以KRaft模式快速部署Kafka
  • 使用Avalonia UI实现DataGrid