我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Java 内存模型(JVM)

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Java 内存模型(JVM)

前言

在并发编程中,当多个线程同时访问同一个共享的可变变量时,会产生不确定的结果,所以要编写线程安全的代码,其本质上是对这些可变的共享变量的访问操作进行管理。导致这种不确定结果的原因就是可见性、有序性和原子性问题,Java 为解决可见性和有序性问题引入了 Java 内存模型,使用互斥方案(其核心实现技术是锁)来解决原子性问题。这篇先来看看解决可见性、有序性问题的 Java 内存模型(JMM)。

一、什么是 Java 内存模型

Java 内存模型定义如下:

内存模型限制的是共享变量,也就是存储在堆内存中的变量,在 Java 语言中,所有的实例变量、静态变量和数组元素都存储在堆内存之中。而方法参数、异常处理参数这些局部变量存储在方法栈帧之中,因此不会在线程之间共享,不会受到内存模型影响,也不存在内存可见性问题。

通常,在线程之间的通讯方式有共享内存和消息传递两种,很明显,Java 采用的是第一种即共享的内存模型,在共享的内存模型里,多线程之间共享程序的公共状态,通过读-写内存的方式来进行隐式通讯。

从抽象的角度来看,JMM 其实是定义了线程和主内存之间的关系,首先,多个线程之间的共享变量存储在主内存之中,同时每个线程都有一个自己私有的本地内存,本地内存中存储着该线程读或写共享变量的副本(注意:本地内存是 JMM 定义的抽象概念,实际上并不存在)。抽象模型如下图所示:

在这个抽象的内存模型中,在两个线程之间的通信(共享变量状态变更)时,会进行如下两个步骤:

  1. 线程 A 把在本地内存更新后的共享变量副本的值,刷新到主内存中。
  2. 线程 B 在使用到该共享变量时,到主内存中去读取线程 A 更新后的共享变量的值,并更新线程 B 本地内存的值。

JMM 本质上是在硬件(处理器)内存模型之上又做了一层抽象,使得应用开发人员只需要了解 JMM 就可以编写出正确的并发代码,而无需过多了解硬件层面的内存模型。

二、为什么需要 Java 内存模型

在日常的程序开发中,为一些共享变量赋值的场景会经常碰到,假设一个线程为整型共享变量 count 做赋值操作(count = 9527;),此时就会有一个问题,其它读取该共享变量的线程在什么情况下获取到的变量值为 9527 呢?如果缺少同步的话,会有很多因素导致其它读取该变量的线程无法立即甚至是永远都无法看到该变量的最新值。

比如缓存就可能会改变写入共享变量副本提交到主内存的次序,保存在本地缓存的值,对于其它线程是不可见的;编译器为了优化性能,有时候会改变程序中语句执行的先后顺序,这些因素都有可能会导致其它线程无法看到共享变量的最新值。

在文章开头,提到了 JMM 主要是为了解决可见性和有序性问题,那么首先就要先搞清楚,导致可见性和有序性问题发生的本质原因是什么?现在的服务绝大部分都是运行在多核 CPU 的服务器上,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据就会有一致性问题了,当一个线程对共享变量的修改,另外一个线程无法立刻看到。导致可见性问题的本质原因是缓存

有序性是指代码实际的执行顺序和代码定义的顺序一致,编译器为了优化性能,虽然会遵守 as-if-serial 语义(不管怎么重排序,在单线程下的执行结果不能改变),不过有时候编译器及解释器的优化也可能引发一些问题。比如:双重检查来创建单实例对象。下面是使用双重检查来实现延迟创建单例对象的代码:



public class DoubleCheckedInstance {

  private static DoubleCheckedInstance instance;

  public static DoubleCheckedInstance getInstance() {
    if (instance == null) {
      synchronized (DoubleCheckedInstance.class) {
        if (instance == null) {
          instance = new DoubleCheckedInstance();
        }
      }
    }

    return instance;
  }
  
}

这里的 instance = new DoubleCheckedInstance();,看起来 Java 代码只有一行,应该是无法就行重排序的,实际上其编译后的实际指令是如下三步:

  1. 分配对象的内存空间
  2. 初始化对象
  3. 设置 instance 指向刚刚已经分配的内存地址

上面的第 2 步和第 3 步如果改变执行顺序也不会改变单线程的执行结果,也就是说可能会发生重排序,下图是一种多线程并发执行的场景:

此时线程 B 获取到的 instance 是没有初始化过的,如果此来访问 instance 的成员变量就可能触发空指针异常。导致有序性问题的本质原因是编译器优化。那你可能会想既然缓存和编译器优化是导致可见性问题和有序性问题的原因,那直接禁用掉不就可以彻底解决这些问题了吗,但是如果这么做了的话,程序的性能可能就会受到比较大的影响了。

其实可以换一种思路,能不能把这些禁用缓存和编译器优化的权利交给编码的工程师来处理,他们肯定最清楚什么时候需要禁用,这样就只需要提供按需禁用缓存和编译优化的方法即可,使用比较灵活。因此Java 内存模型就诞生了,它规范了 JVM 如何提供按需禁用缓存和编译优化的方法,规定了 JVM 必须遵守一组最小的保证,这个最小保证规定了线程对共享变量的写入操作何时对其它线程可见。

三、顺序一致性内存模型

顺序一致性模型是一个理想化后的理论参考模型,处理器和编程语言的内存模型的设计都是参考的顺序一致性模型理论。其有如下两大特性:

  1. 一个线程中的所有操作必须按照程序的顺序来执行
  2. 所有的线程都只能看到一个单一的执行操作顺序,不管程序是否同步

在工程师视角下的顺序一致性模型如下:

顺序一致性模型有一个单一的全局内存,这个全局内存可以通过左右摇摆的开关可以连接到任意一个线程,每个线程都必须按照程序的顺序来执行内存的读和写操作。该理想模型下,任务时刻都只能有一个线程可以连接到内存,当多个线程并发执行时,就可以通过开关就可以把多个线程的读和写操作串行化

顺序一致性模型中,所有操操作完全按照顺序串行执行,但是在 JMM 中就没有这个保证了,未同步的程序在 JMM 中不仅程序的执行顺序是无序的,而且由于本地内存的存在,所有线程看到的操作顺序也可能会不一致,比如一个线程把写共享变量保存在本地内存中,在还没有刷新到主内存前,其它线程是不可见的,只有更新到主内存后,其它线程才有可能看到。

JMM 对在正确同步的程序做了顺序一致性的保证,也就是程序的执行结果和该程序在顺序一致性内存模型中的执行结果相同。

四、Happens-Before 规则

Happens-Before 规则是 JMM 中的核心概念,Happens-Before 概念最开始在 这篇论文 提出,其在论文中使用 Happens-Before 来定义分布式系统之间的偏序关系。在 JSR-133 中使用 Happens-Before 来指定两个操作之间的执行顺序。

JMM 正是通过这个规则来保证跨线程的内存可见性,Happens-Before 的含义是前面一个对共享变量的操作结果对该变量的后续操作是可见的,约束了编译器的优化行为,虽然允许编译器优化,但是优化后的代码必须要满足 Happens-Before 规则,这个规则给工程师做了这个保证:同步的多线程程序是按照 Happens-Before 指定的顺序来执行的。目的就是为了在不改变程序(单线程或者正确同步的多线程程序)执行结果的前提下,尽最大可能的提高程序执行的效率。

JSR-133 规范中定了如下 6 项 Happens-Before 规则:

  1. 程序顺序规则:一个线程中的每个操作,Happens-Before 该线程中的任意后续操作
  2. 监视器锁规则:对一个锁的解锁操作,Happens-Before 于后面对这个锁的加锁操作
  3. volatile 规则:对一个 volatile 类型的变量的写操作,Happens-Before 与任意后面对这个 volatile 变量的读操作
  4. 传递性规则:如果操作 A Happens-Before 于操作 B,并且操作 B Happens-Before 于操作 C,则操作 A Happens-Before 于操作 C
  5. start() 规则:如果一个线程 A 执行操作 threadB.start() 启动线程 B,那么线程 A 的 start() 操作 Happens-Before 于线程 B 的任意操作
  6. join() 规则:如果线程 A 执行操作 threadB.join() 并成功返回,那么线程 B 中的任意操作 Happens-Before 于线程 A 从 threadB.join() 操作成功返回

JMM 的一个基本原则是:只要不改变单线程和正确同步的多线程的执行结果,编译器和处理器随便怎么优化都可以,实际上对于应用开发人员对于两个操作是否真的被重排序并不关心,真正关心的是执行结果不能被修改。因此 Happens-Before 本质上和 sa-if-serial 的语义是一致的,只是 sa-if-serial 只是保证在单线程下的执行结果不被改变。

总结:
本文主要介绍了内存模型的相关基础知识和相关概念,JMM 屏蔽了不同处理器内存模型之间的差异,在不同的处理器平台上给应用开发人员抽象出了统一的 Java 内存模型(JMM)。常见的处理器内存模型比 JMM 的要弱,因此 JVM 会在生成字节码指令时在适当的位置插入内存屏障(内存屏障的类型会因处理器平台而有所不同)来限制部分重排序。更多关于Java 内存模型的资料请关注编程网其它相关文章!,希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Java 内存模型(JVM)

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Java JVM 内存模型包含哪些内容?(java jvm内存模型有哪些)

在Java编程中,Java虚拟机(JVM)的内存模型是一个非常重要的概念。它定义了Java程序在运行时内存的分配、使用和管理方式。理解JVM内存模型对于编写高效、稳定的Java程序至关重要。一、JVM内存模型的基本概念
Java JVM 内存模型包含哪些内容?(java jvm内存模型有哪些)
Javajvm2024-12-18

java jvm内存模型有哪些

Java虚拟机(JVM)内存模型主要有以下几个部分:1. 堆内存(Heap):用于存储Java对象的实例以及数组。堆内存是所有线程共享的,是Java程序运行时的动态数据区。堆内存被划分为新生代(Young Generation)和老年代(O
2023-10-12

【JVM】JVM内存模型(详细)

目录 一.JVM概述1.jvm简介2.jvm作用3.jvm的内存模型 二.类加载器1.类加载器的作用2.加载器的类型3.双亲委派机制的运行过程4.双亲委派机制优缺点5.为什么要破坏双亲委派机制6.破坏双亲委派机制的方式 三.
2023-08-16

jvm中java内存模型的示例分析

这篇文章主要介绍了jvm中java内存模型的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。一、java内存模型和java内存结构有什么区别 1、java内存结构记得是
2023-06-19

什么是JVM内存模型?

本篇文章带大家初步了解一下JVM内存模型,有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。计算机内存模型在程序运行时,CPU通过访问主存获取数据,但随着CPU的快速发展,CPU访问速度越来越高,硬件无法满足CPU的条件下,大多内存加入了高速缓存
什么是JVM内存模型?
2016-08-29

JVM(Java虚拟机)详解(JVM 内存模型、堆、GC、直接内存、性能调优)

JVM(Java虚拟机) JVM 内存模型 结构图 jdk1.8 结构图(极简) jdk1.8 结构图(简单) JVM(Java虚拟机): 是一个抽象的计算模型。如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域。
2023-08-30

深入理解JVM内存模型

JVM内存结构包括方法区、堆、栈、本地方法栈和程序计数器。不同的内存区域有不同的作用和管理方式,合理地使用和管理内存是编写高效、稳定的Java程序的重要方面。
JVM内存2024-11-30

Java常见知识点中Jvm内存结构、Java内存模型、Java对象模型的区别是什么

这篇文章将为大家详细讲解有关Java常见知识点中Jvm内存结构、Java内存模型、Java对象模型的区别是什么,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。我们都知道,Java代码是要运行在
2023-06-05

java jvm内存模型的操作方法有哪些

Java虚拟机(JVM)的内存模型操作方法主要有以下几种:1. 堆内存管理:Java堆是JVM管理的最大的一块内存区域,用于存储对象实例。可以通过-Xmx和-Xms参数来设置堆的最大和初始大小,并通过垃圾回收机制来管理堆内存的分配和释放。2
2023-10-18

面试必问,JVM内存模型扫盲

运行时常量池(Runtime Constant Pool)是方法区中的一部分,用于存储编译期间生成的各种字面量和符号引用。在Java程序运行时,JVM将编译期生成的class文件中的常量池内容读取到运行时常量池中。
JVM内存模型2024-11-30

如何进行JVM内存模型使用

这篇文章给大家介绍如何进行JVM内存模型使用,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。你对JVM内存模型是否熟悉,这里和大家分享一下,主要包括栈和堆两部分内容,Java栈是与每一个线程关联的,JVM在创建每一个线程
2023-06-17

JVM 运行时数据区与JMM 内存模型

这篇文章主要介绍了JVM 运行时数据区与JMM 内存模型,文章围绕主题展开详细的内容介绍,具有一定的参考价值。需要的朋友可以参考一下
2022-11-13

一文带你深入理解JVM内存模型

在共享内存的并发模型里面,线程之间共享程序的公共状态,线程之间通过读写内存中公共状态来进行隐式通信。
JVM内存模型2024-12-14

Java内存模型详解

JMM全称JavaMemoryModel,中文翻译Java内存模型,一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,本详细介绍了Java内存模型,感兴趣的同学可以参考一下
2023-05-18

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录