【java】Java 内存模型

news2024/9/30 15:23:27

文章目录

  • 前言
  • 什么是 Java 内存模型
  • 为什么需要 Java 内存模型
  • 顺序一致性内存模型
  • Happens-Before 规则
  • 总结

前言

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

什么是 Java 内存模型

Java 内存模型在维基百科上的定义如下:

The Java memory model describes how threads in the Java programming
language interact through memory. Together with the description of
single-threaded execution of code, the memory model provides the
semantics of the Java programming language.

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

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

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

1.png

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

线程 A 把在本地内存更新后的共享变量副本的值,刷新到主内存中。
线程 B 在使用到该共享变量时,到主内存中去读取线程 A 更新后的共享变量的值,并更新线程 B 本地内存的值。
JMM 本质上是在硬件(处理器)内存模型之上又做了一层抽象,使得应用开发人员只需要了解 JMM 就可以编写出正确的并发代码,而无需过多了解硬件层面的内存模型。

为什么需要 Java 内存模型

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

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

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

2.png

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

/**
 * @author mghio
 * @since 2021-08-22
 */
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 步如果改变执行顺序也不会改变单线程的执行结果,也就是说可能会发生重排序,下图是一种多线程并发执行的场景:

3.png

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

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

顺序一致性内存模型

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

一个线程中的所有操作必须按照程序的顺序来执行
所有的线程都只能看到一个单一的执行操作顺序,不管程序是否同步
在工程师视角下的顺序一致性模型如下:

4.png

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

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

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

Happens-Before 规则

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

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

5.png

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

程序顺序规则:一个线程中的每个操作,Happens-Before 该线程中的任意后续操作
监视器锁规则:对一个锁的解锁操作,Happens-Before 于后面对这个锁的加锁操作
volatile 规则对一个 volatile 类型的变量的写操作,Happens-Before 与任意后面对这个 volatile 变量的读操作
传递性规则:如果操作 A Happens-Before 于操作 B,并且操作 B Happens-Before 于操作 C,则操作 A Happens-Before 于操作 C
start() 规则:如果一个线程 A 执行操作 threadB.start() 启动线程 B,那么线程 A 的 start() 操作 Happens-Before 于线程 B 的任意操作
join() 规则:如果线程 A 执行操作 threadB.join() 并成功返回,那么线程 B 中的任意操作 Happens-Before 于线程 A 从 threadB.join() 操作成功返回
JMM 的一个基本原则是:只要不改变单线程和正确同步的多线程的执行结果,编译器和处理器随便怎么优化都可以,实际上对于应用开发人员对于两个操作是否真的被重排序并不关心,真正关心的是执行结果不能被修改。因此 Happens-Before 本质上和 sa-if-serial 的语义是一致的,只是 sa-if-serial 只是保证在单线程下的执行结果不被改变。

总结

本文主要介绍了内存模型的相关基础知识和相关概念,JMM 屏蔽了不同处理器内存模型之间的差异,在不同的处理器平台上给应用开发人员抽象出了统一的 Java 内存模型(JMM)。常见的处理器内存模型比 JMM 的要弱,因此 JVM 会在生成字节码指令时在适当的位置插入内存屏障(内存屏障的类型会因处理器平台而有所不同)来限制部分重排序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/374983.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言青蛙跳台阶【图文详解】

青蛙跳台阶前言1. 题目介绍2. 解题思路3. 利用图片来演示青蛙跳台阶的原理4. 如何用C语言实现青蛙跳台阶前言 在本文,我们要与一只活泼可爱的小青蛙合作,带领着它跳上台阶,这个小家伙精力充沛,特别擅长于跳跃。我们要让它做我们的…

一个诡异的 Pulsar InterruptedException 异常

背景 今天收到业务团队反馈线上有个应用往 Pulsar 中发送消息失败了,经过日志查看得知是发送消息时候抛出了 java.lang.InterruptedException 异常。 和业务沟通后得知是在一个 gRPC 接口中触发的消息发送,大约持续了半个小时的异常后便恢复正常了&…

MySQL数据库中的函数怎样使用?

函数 是指一段可以直接被另一段程序调用的程序或代码。 也就意味着,这一段程序或代码在MySQL中已经给我们提供了,我们要做的就是在合适的业务场景调用对应的函数完成对应的业务需求即可。 那么,函数到底在哪儿使用呢?我们先来看两个场景&…

前端开发:JS的节流与防抖

前言 在前端实际开发中,有关JS原生的节流和防抖处理也是很重要的点,关于底层和原理的掌握使用,尤其是在性能优化方面甚为重要。作为前端开发的进阶内容,在实际开发过程中节流和防抖通常都是项目优化的必要手段,而且也是…

【Project】项目管理软件学习笔记

一、前言使用Project制定项目计划步骤大致如下:以Project2013为例,按照上图步骤指定项目计划。二、实施2.1 创建空白项目点击文件——新建——空白项目,即完成了空白项目的创建,在此我把该项目保存为60mm项目管理.mpp,…

深入浅出1588v2(PTP)里的时间同步原理

1.时间同步1.1 单步同步(OneStep)单步同步最为简单,master向slave发送一个sync的同步包,同步包里带有这条信息发送时master的当前时间t1,假如这条信息从master传输到slave需要的传输时间是D,那么slave收到信息时,maste…

芯驰(E3-gateway)开发板环境搭建

1-Windows下环境配置 可以在Windows上使用命令行或者IAR IDE编译SSDK项目。Windows编译依赖的工具已经包含在 prebuilts/windows 目录中,包括编译器、Python和命令行工具。 1.1.1 CMD SSDK集成 msys 工具,可以在Windows命令行中完成SDK的配置、编译和…

嵌入式系统硬件设计与实践(第一步下载eda软件)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing 163.com】 现实生活中,我们经常发现有的人定了很多的目标,但是到最后一个都没有实现。这听上去有点奇怪,但确实是实实在在…

Mysql数据库总结

一.MySQL 的基础1.架构图Mysql逻辑架构图主要分三层:(1)第一层负责连接处理,授权认证,安全等等 (2)第二层负责编译并优化SQL (3)第三层是存储引擎。Mysql 服务器的默认端…

Welcome to TryHackMe --- 我在TryHackMe学习的第90天

我在TryHackMe学习的第90天 自发的thm玩家交流企鹅群:751273347 TryHackMe是一个及其优秀的道德嗨客学习平台 这三个月里,我在TryHackMe都学了什么 TryHackMe的几个路径我觉得是按照oscp出的,所以理论上讲我应该差不多有oscp水准&#xff…

ElasticSearch修改索引字段类型

一、Es报MapperParsingException异常 线上功能报错,一看日志是往es中添加数据报错,错误日志如下: org.elasticsearch.index.mapper.MapperParsingException: failed to parse field [categoryId] of type [integer] in document with id 16…

软件技术知识库必备的功能清单及注意事项!

文档是一个迭代过程。它可能需要根据客户反馈进行改进,或者可能需要折射文档中已包含的某些内容。知识库可以包括客户的常见问题或对解决方案的更多参考,这些解决方案可能需要包括这些解决方案,以提高效率、生产力并降低公司成本,…

百趣代谢组学分享—揭示水稻“生长-防御”平衡调控机制!

湖南农业大学科研团队揭示水稻“生长—防御”平衡调控机制! 文章标题:Rice cellulose synthase-like protein OsCSLD4 coordinates the trade-off between plant growth and defense 发表期刊:Frontiers in Plant Science 影响因子&#x…

【个人总结】超详细Neo4j安装下载

【个人总结】超详细Neo4j安装下载一、下载1.1 Jdk下载1.2 Neo4j下载:二、安装配置2.1 解压2.2 配置三、启动Neo4j一、下载 1.1 Jdk下载 下载neo4j之前,需要下载jdk,这里默认已经下载过jdk,,若未下载可参考之前文章:h…

17.标准库特殊设施

文章目录标准库特殊设施17.1tuple类型17.1.1定义和初始化tuple访问tuple的成员关系和相等运算符17.1.2使用tuple返回多个值17.2bitset类型(后续需要时再详细了解)17.3正则表达式17.4随机数bernoulli_distribution类17.5IO库再探标准库特殊设施 17.1tuple类型 tuple(定义在tupl…

Spark工作原理

1)Spark工作原理: 首先看中间是一个Spark集群,可以理解为是Spark的 standalone集群,集群中有6个节点 左边是Spark的客户端节点,这个节点主要负责向Spark集群提交任务,假设在这里我们向Spark集群提交了一个任…

周赛334(前缀和、贪心+双指针、Dijkstra求最短路径、二分答案)

文章目录[6369. 左右元素和的差值](https://leetcode.cn/problems/left-and-right-sum-differences/)前缀和[6368. 找出字符串的可整除数组](https://leetcode.cn/problems/find-the-divisibility-array-of-a-string/)超长整数如何取余?[6367. 求出最多标记下标](ht…

9.3 IGMPv3

实验目的 熟悉IGMPv3的应用场景掌握IGMPv3的配置方法实验拓扑 实验拓扑如图9-22所示: 图9-22:IGMPv3 实验步骤 (1)配置IP地址 MCS1的配置 MCS1的IP地址配置如图9-23所示: 图9-23:配置MCS1的IP地址 MCS2…

结构体字节对齐、偏移量

复习下struct的大小、成员偏移量offsetof,说下我的理解: 64位下默认对齐数default8原则1:struct中每一个成员变量tmp的对齐数realmin{default,tmp} struct Student {int num;//0char name[8];double score; } stu; 这个结构体stu中&#x…

阿里前端二面经典手写面试题汇总

实现类的继承 实现类的继承-简版 类的继承在几年前是重点内容,有n种继承方式各有优劣,es6普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想…