Java内存模型----JMM

news2024/11/17 5:17:32

Java内存模型

  • 1. 前言
  • 2. 主内存与工作内存
  • 3. JMM解决什么问题?
  • 4. JMM内存交互
  • 5. Happens-Before
    • 1. 程序的顺序性规则
    • 2. volatile 变量规则
    • 3. 管程中锁的规则
    • 4. 线程启动规则
    • 5. 线程join规则
    • 6. 其他规则

1. 前言

  1. 内存模型这个概念。我们可以理解为: 在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不同架构的物理计算机可以有不 一样的内存模型,JVM 也有自己的内存模型。

  2. JVM 中试图定义一种 Java 内存模型(Java Memory Model,JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序 在各种平台下都能 达到一致的内存访问效果

  3. 从开发者角度而言, Java内存模型描述了在多线程代码中哪些行为是合 法的,以及线程如何通过内存进行交互。它描述了“程序中的变量“ 和 ”从内存或 者寄存器获取或存储它们的底层细节”之间的关系。

  4. Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体 来说,这些方法包括 volatilesynchronizedfinal 三个关键字,以及Happens-Before 规则

2. 主内存与工作内存

1.JMM 的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与 Java 编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数值对 象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题.。

注意点如下:

  1. 变量不是我们在代码中·int i = 0;这种变量。
  2. 这讲的是共享。不共享的不会出问题。

2.JMM 规定了所有的变量都存储在主内存(Main Memory)中

3.每条线程还有自己的工作内存(Working Memory)工作内存中保留了 该线程使用到的变量的主内存的副本。工作内存是 JMM 的一个抽象概念,并不 真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
在这里插入图片描述
4.线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存 中的变量。不同的线程间也无法直接访问对方工作内存中的变量,线程间变量 值的传递均需要通过主内存来完成

注意:为了获得较好的执行效能

  1. JMM 并没有限制执行引擎使用处理器的特定寄存器或缓存来和主 存进行交互
  2. JMM 也没有限制即时编译器调整指令执行顺序这类优化措施

3. JMM解决什么问题?

  1. 工作内存数据一致性:可见性问题

    • 各个线程操作数据时会使用工作内存中的主内存中共享变量副本,当多个 线程的运算任务都涉及同一个共享变量时,可能导致各自的共享变量副本不一 致。如果真的发生这种情况,数据同步回主内存以谁的副本数据为准?
    • Java 内存模型主要通过一系列的数据同步协议、规则来保证数据的一致 性。
  2. 约束指令重排序优化:有序性问题

Java 中重排序通常是编译器或运行时环境为了优化程序性能而采取的对指 令进行重新排序执行的一种手段。重排序可分为两类:编译期重排序和运行期 重排序(处理器乱序优化),分别对应编译时和运行时环境

同样的,指令重排序不是随意重排序,它需要满足以下几个条件:

  • 在单线程环境下不能改变程序运行的结果。即时编译器(和处理器)需 要保证程序能够遵守 as-if-serial 属性。通俗地说,就是在单线程情 况下,要给程序一个顺序执行的假象。即使经过重排序后的执行结果要 与顺序执行的结果保持一致。
  • 存在数据依赖关系的不允许重排序。
  • 多线程环境下,如果线程处理逻辑之间存在依赖关系,有可能因为指令 重排序导致运行结果与预期不同。

4. JMM内存交互

1.JMM 定义了 8 个操作来完成主内存和工作内存之间的交互操作。 JVM 实现 时必须保证下面介绍的每种操作都是 原子的(对于 double 和 long 型的变量来 说, load、store、 read、和 write 操作在某些平台上允许有例外 )。

  • lock (锁定) - 作用于主内存的变量,它把一个变量标识为一条线程独 占的状态
  • unlock (解锁) - 作用于主内存的变量,它把一个处于锁定状态的变量 释放出来,释放后的变量才可以被其他线程锁定。
  • read (读取) - 作用于主内存的变量,它把一个变量的值从主内存传输 到线程的工作内存中,以便随后的 load 动作使用。
  • load (载入) - 作用于工作内存的变量,它把 read 操作从主内存中得到 的变量值放入工作内存的变量副本中。
  • use (使用) - 作用于工作内存的变量,它把工作内存中一个变量的值传 递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令 时就会执行这个操作。
  • assign (赋值) - 作用于工作内存的变量,它把一个从执行引擎接收到 的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store (存储) - 作用于工作内存的变量,它把工作内存中一个变量的值 传送到主内存中,以便随后 write 操作使用。
  • write (写入) - 作用于主内存的变量,它把 store 操作从工作内存中得 到的变量的值放入主内存的变量中。

2.如果要把一个变量从主内存中复制到工作内存,就需要按序执行 read load 操作;如果把变量从工作内存中同步回主内存中,就需要按序执行storewrite 操作。但 Java 内存模型只要求上述操作必须按顺序执行,而 没有保证必须是连续执行

3.JMM 还规定了上述 8 种基本操作,需要满足以下规则:

  • readload 必须成对出现;store 和 write 必须成对出现。即不允许 一个变量从主内存读取了但工作内存不接受,或从工作内存发起回写了 但主内存不接受的情况出现。
  • 不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改 变了之后必须把变化同步到主内存中。
  • 不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从工 作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个 未被初始化(load 或assign )的变量。换句话说,就是对一个变量实 施 use 和 store 操作之前,必须先执行过了 load 或 assign 操作。
  • 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操 作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同 次数的 unlock 操作,变量才会被解锁。所以 lock 和 unlock 必须成对 出现。
  • 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在 执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变 量的值
  • 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操 作,也不允许去 unlock 一个被其他线程锁定的变量。
  • 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中 (执行 store 和 write 操作)

4.整体如下图所示:
在这里插入图片描述

5. Happens-Before

1.Java 内存模型里面,最晦涩的部分就是 Happens-Before 规则了,Happens-Before 规则最初是在一篇叫做 Time, Clocks, and the Ordering of Events in a Distributed System 的论文中提出来的,在这篇论文中, Happens- Before 的语义是一种因果关系

2.如何来理解Happens-Before呢?如果就字面意思的话网上很多文章都翻译 称:先行发生, Happens-Before 并不是说前面一个操作发生在后续操作的 面,它真正要表达的是: 前面一个操作的结果对后续操作是可见的 打个比方: A Happens-Before B,可表明A操作的结果对B是可见的。

3.Happens-Before有一个特性就是传递性:即 A Happens-Before B , B Happens-Before C,则 A Happens-Before C .

4.Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求 编译器优化后一定遵守 Happens-Before 规则,具体的一些规则如下:

1. 程序的顺序性规则

  • 这条规则是指在一个线程中,按照程序顺序(可能是重排序后的顺序), 前面的操作 Happens-Before 于后续的任意操作,程序前面对某个变量的修改 一定是对后续操作可见的:
ClassReordering {
	int x = 0, y = 0;
	public void writer() {
		x = 1;
		y = 2; 
	}
	public void reader() { 
		int r1 = y;
		int r2 = x;
	} 
}

2. volatile 变量规则

  • 这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对 这个 volatile 变量的读操作:
class VolatileExample { 
	int x = 0;
	volatile boolean v = false;
	// 线程A 先
	public void writer() { 
		x = 42;
		v = true; 
	}
	// 线程B 后
	public void reader() { 
		if (v == true) {
		// 这里x会是多少呢?
		} 
	}
}
  • 特别注意

    1. 我们声明一个 volatile 变量 volatile int x = 0,它表达的是:告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入
    2. volatile 可以用来解决可见性问题
  • 这里有两点:

    • 线程B能看到线程A对变量v的写结果
    • 结合顺序性规则和传递性特性可知在线程B中仍然能得到x的值为42
  • 注意:第二点只有从jdk1.5开始才能满足,因为Java 内存模型在 1.5 版本对 volatile 语义进行了增强(禁止指令重排),1.5以前有可能x的值还为0。

3. 管程中锁的规则

  • 对一个锁的解锁 Happens-Before 于后续对这个锁的加锁

1.大致了解一下什么是管程

  1. 管程(Monitors,也称为监视器),是一种通用的同步原语,能够实现对 共享资源的互斥访问, Java 中指的就是 synchronized ,synchronized 是 Java 里对管程的实现。
  2. 管程中的锁在 Java 里是隐式实现的,例如下面的代码,在进入同步块之 前,会自动加锁,而在代码块执行完会自动释放锁,加锁以及释放锁都是编译 器帮我们实现的
int x = 10;
public void syn() {
	synchronized (this) { //此处自动加锁
		if (this.x < 12) {
			this.x = 12; 
		}
	} //此处自动解锁 
}

2.从这个规则我们可以得出,释放锁之后,同步代码块中的操作结果对后续 加锁时是可见的。同时结合前面讲的JMM内存操作可知, unlock时会将变量从 工作内存刷到主内存中,获取锁时会从主内存中去读取变量值到工作内存中, 也能证明锁的解锁 Happens-Before 于后续对这个锁的加锁

4. 线程启动规则

  • 它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作
static int var = 66;
	// 主线程A
	public static void t1() {
		Thread B = new Thread(()->{
		// 主线程调用B.start()之前
		// 所有对共享变量的修改,此处皆可见 // 此例中,var==77
	});
	// 此处对共享变量var修改
	var = 77;
	// 主线程启动子线程
	B.start(); 
}

5. 线程join规则

  • 它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够 看到子线程的操作。当然所谓的“看到” ,指的是对共享变量的操作结果可见。
static int var = 55;

//主线程A
public static void t1() {
	Thread B = new Thread(()->{
		// 此处对共享变量var修改
		var = 66; 
	});

	// 主线程启动子线程
	B.start();
	//主线程等待子线程B结束
	B.join()
	// 子线程所有对共享变量的修改
	// 在主线程调用B.join()之后皆可见 // 此例中,var==66
}

6. 其他规则

  1. 线程中断规则: 对线程 interrupt()方法的调用 Happens-Before 被中断线程的代码检测 到中断事件的发生,比如我们可以通过Thread.interrupted()/isInterrupted方法检测到是否有中断发生。
  2. 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的 开始。

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

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

相关文章

springboot基础及上传组件封装

简介 本文主要以文件上传为demo&#xff0c;介绍了一些 springboot web 开发的入门的技术栈。 对应刚接触 springboot 的可以参考下。 主要包括文件md5比对、生成图片缩略图、数据库迁移、文件记录持久化、请求全局异常处理等功能。 准备工作 在 idea 中创建项目&#xff…

PPP认证两种:PAP和CHAP,两次握手和三次握手

CHAP&#xff08;Challenge-Handshake Authentication Protocol&#xff0c;质询握手认证协议&#xff09;的设计理念是增强网络认证过程的安全性。在CHAP的三次握手过程中&#xff0c;不直接传送用户的明文密码&#xff0c;以此来提高安全性&#xff0c;具体步骤如下&#xff…

社交媒体数据恢复:QQ空间

本教程将指导您如何恢复QQ空间中的说说、日志和照片等内容。请注意&#xff0c;本教程不涉及推荐任何数据恢复软件。 一、恢复QQ空间说说 登录您的QQ账号&#xff0c;并进入QQ空间。点击“日志”选项&#xff0c;进入空间日志页面。在空间日志页面&#xff0c;您会看到一个“…

MyBatis源码分析--02:SqlSession建立过程

我们再来看看MyBatis使用流程&#xff1a; InputStream inputStream Resources.getResourceAsStream("myBatis_config.xml"); SqlSessionFactory sqlSessionFactory new SqlSessionFactoryBuilder().build(inputStream); SqlSession session sqlSessionFactory.op…

【Rust日报】关于在其它语言中(特别是新语言中)能否直接调用Rust现有生态的研究...

关于在其它语言中&#xff08;特别是新语言中&#xff09;能否直接调用Rust现有生态的研究 直接调用&#xff0c;也就是不用写FFI绑定库。这篇文章讨论了其中的一些目前的困难&#xff0c;可以操作的一些方法。 本篇是第一篇&#xff1a;https://verdagon.dev/blog/exploring-s…

数学建模 —— 插值与拟合(1)

一、matlab画图 1.1 plot&#xff08;二维图形&#xff09; plot(x) —— 缺省自变量绘图格式 plot(x,y) —— 基本格式&#xff0c;以y(x)的函数关系作出直角坐标图&#xff0c;如果y为nm的矩阵&#xff0c;则以x为自变量&#xff0c;作出m条曲线 plot(x1,y1,x2,y2,…,xn,…

Lesson6--排序(初级数据结构完结篇)

【本节目标】 1. 排序的概念及其运用 2. 常见排序算法的实现 3. 排序算法复杂度及稳定性分析 1.排序的概念及其运用 1.1排序的概念 排序 &#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来…

GPT LoRA 大模型微调,生成猫耳娘

往期热门专栏回顾 专栏描述Java项目实战介绍Java组件安装、使用&#xff1b;手写框架等Aws服务器实战Aws Linux服务器上操作nginx、git、JDK、VueJava微服务实战Java 微服务实战&#xff0c;Spring Cloud Netflix套件、Spring Cloud Alibaba套件、Seata、gateway、shadingjdbc…

MACOS安装 vue 抱错解决方法npm ERR! code EACCESnpm ERR! syscall mkdirnpm ERR!

问题 在使用脚手架 vue-cli 创建 vue 工程的时候存在权限不足的情况下&#xff0c;报错&#xff1b; npm error code EACCES npm error syscall open npm error path /Users/ npm ERR! code EACCESnpm ERR! syscall mkdirnpm ERR! 解决方案&#xff1a; sudo npm cache cl…

什么是最好的手机数据恢复软件?6 款手机数据恢复软件 [2024 年更新]

什么是最好的手机数据恢复软件&#xff1f;在这篇文章中&#xff0c;您将了解 6 款最好的免费手机数据恢复软件&#xff0c;并学习如何恢复数据的完整指南。 最好的手机数据恢复软件是什么&#xff1f; 手机数据恢复软件是恢复智能手机中丢失或删除的文件、消息、照片和其他宝…

反射获取成员变量

目录 利用反射获取成员变量 ​编辑 代码实现 获取class对象 获取成员变量 获取单个成员变量 获取成员变量的名字 获取权限修饰符 获取成员变量的数据类型 获取成员变量记录的值 修改对象里面记录的值 利用反射获取成员变量 代码实现 Student类&#xff1a; 获取clas…

【 0 基础 Docker 极速入门】镜像、容器、常用命令总结

Docker Images&#xff08;镜像&#xff09;生命周期 Docker 是一个用于创建、部署和运行应用容器的平台。为了更好地理解 Docker 的生命周期&#xff0c;以下是相关概念的介绍&#xff0c;并说明它们如何相互关联&#xff1a; Docker&#xff1a; Docker 是一个开源平台&#…

生成ssh密钥,使用ssh连接linux系统

这里写目录标题 ssh密钥大概介绍1、密钥在哪里生成&#xff08;客户端/服务器&#xff09;&#xff1f;2、密钥生成是什么样子的&#xff1f; ssh &#xff08;生成密钥、密钥传输、配置连接、连接服务&#xff09;过程1、生成密钥提示一&#xff1a;输入保存密钥的文件&#x…

Nginx实战:nginx支持带下划线的header

nginx对header 的名字字符做了限制&#xff0c;默认 underscores_in_headers 为off&#xff0c;表示如果header name中包含下划线&#xff0c;则忽略掉&#xff0c;后端服务就获取不到该请求头。 为了支持header带下划线的参数&#xff0c;可以在http内或者server内设置如下参数…

ip地址告诉别人安全吗?ip地址告诉别人会有什么风险

IP地址告诉别人安全吗&#xff1f;在数字化时代&#xff0c;IP地址作为网络连接的关键标识符&#xff0c;承载着重要的安全意义。然而&#xff0c;很多人可能并不清楚&#xff0c;轻易地将自己的IP地址告诉他人可能带来一系列安全风险。那么&#xff0c;IP地址告诉别人会有什么…

7-11 验证宏(verify)---PTA实验C++

一、题目描述 无需惊慌&#xff0c;C缔造者Bjarne Stroustrup这句话说的CPP是C PreProcessor&#xff0c;即“C代码预处理器”。CPP根据#include #define #if #pragma等指令对文件进行处理。这种处理发生在编译之前&#xff0c;所以CPP叫“预处理器”。CPP完全是文本层面的处理…

少样本学习与零样本学习:理解与应用

少样本学习与零样本学习&#xff1a;理解与应用 在现代机器学习领域中&#xff0c;少样本学习&#xff08;Few-Shot Learning&#xff09;和零样本学习&#xff08;Zero-Shot Learning&#xff09;正变得越来越重要。这些技术能够在数据稀缺的情况下有效地进行学习和推理&…

禁止Windows Defender任务计划程序

开始键->搜索“任务计划程序”->“任务计划程序库”->“Microsoft”->"Windows"->"Windows Defender"->右边四项

Prometheus + Grafana + Alertmanager 系统监控

PrometheusGrafana 系统监控 1. 简介1.1 Prometheus 普罗 米修斯1.2 Grafana 2. 快速试用2.1 Prometheus 普罗 米修斯2.2 Prometheus 配置文件2.3 Grafana 2. 使用 Docker-Compose脚本部署监控服务3. Grafana 配置3.1 配置数据源 Prometheus3.2 使用模板ID 配置监控模板3.3 使用…

【SpringBoot】怎么在一个大的SpringBoot项目中创建多个小的SpringBoot项目,从而形成子父依赖

父子项目工程创建 步骤 先创建父项目 具体操作步骤请看本文章&#xff1a;使用maven工程创建spring boot项目 创建子项目 file- project structure module–new module 剩下步骤请看创建父工程时的操作使用maven工程创建spring boot项目 应用 确认即可 之后创建启动类…