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

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

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

北京

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

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

看不清楚,换张图片

免费获取短信验证码

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

该内存指的是主内存,实际上是物理内存的一小部分

二、JAVA 内存模型的抽象

1、java内存中哪些数据是线程安全的,哪些是非安全的

非线程安全:

在java中所有的实例域、静态域、和数组元素都存放在堆内存中,并且这些数据是线程共享的,所以会存在内存可见性问题

线程安全

局部变量、方法定义的参数、异常处理器参数是当前线程的虚拟机栈中的数据,并且不会进行线程共享,所以不会存在内存可见性问题

2、线程间通讯的本质

线程间通讯的本质是

JMM即JAVA内存模型进行控制,JMM决定了一个线程对共享变量的写入何时对其他线程可见。

由上图能看出来线程间的通讯都是通过主内存来进行传递消息的, 每个线程在进行共享数据处理的时候都是将共享的数据复制到当前线程本地(每个线程自己都有一个内存)来进行操作。

消息通讯过程(不考虑数据安全性的问题)

线程一将主内存中的共享变量 A 加载到自己的本地内存中进行处理。比如 A = 1; 此时将修改的共享变量 A 刷入到主内存中, 之后线程二再将主内存中的共享变量 A 读取到本地内存进行操作; 整个数据交互的过程是JMM控制的,主要控制主内存与每个线程的本地内存如何进行交互来提供共享数据的可见性

三、重排序

程序在执行的时候为了提高效率会将程序指令进行重新排序

1、重排序分类

编译器优化重排序

编译器在不改变单线程程序语义的情况下进行语句执行顺序的优化

指令集并行重排序

如果不存在数据的依赖性的话,处理器可以改变语句对应机器指令的执行顺序

内存系统重排序

由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

2、重排序过程

以上三种重排序都会导致我们在写并发程序的时候出现内存可见性的问题。

JMM的编译器重排序规则会禁止特定类型的编译器重排序;

JMM的处理器重排序规则会要求java编译器在生成指令序列的时候插入特定的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器进行重排序

3、处理器重排序

由于为了避免处理器等待向内存中写入数据的延时,在处理器和内存中间加了一个缓冲区,这样处理器可以一直向缓冲区中写入数据,等到一定时间将缓冲区的数据一次性的刷入到内存中。

优点:

处理器不同停顿,提高了处理器的运行效率

减少在向内存写入数据时的内存总线的占用

缺点:

每个处理器上的写缓冲区只对当前处理器可见,所以就会造成内存操作的执行顺序和实际情况不符合 例如以下场景 :

在当前场景中就可能出现在处理器A和处理器B没有将它们各自的写缓冲区中的数据刷回内存中, 将内存中读取的A=0、B =0进行给X和Y赋值,此时将缓冲区的数据刷入内存,导致了最后结果和实际想要的结果不一致。因为只有将缓冲区的数据刷入到了内存中才叫真正的执行

以上主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,JMM定义了以下8种操作来完成

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

操作执行流程图解:

同步规则分析

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

4、内存屏障指令

为了解决处理器重排序导致的内存错误,java编译器在生成指令序列的适当位置插入内存屏障指令,来禁止特定类型的处理器重排序

内存屏障指令

5、happens-before(先行规则)

happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据

在JMM中如果一个操作中的结果需要对另一个操作可见,那么这两个操作之前必须要存在happens-before关系 (两个操作可以是同一个线程也可以不是一个线程)

规则内容:

程序顺序规则

指的是在一个线程内控制代码顺序,比如分支、循环等,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行

加锁规则

一个解锁(unlock)操作一定要发生于一个加锁(lock)操作之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)

volatile变量规则

对一个volatile的变量的写操作要发生在对这个变量的读操作之前,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值

线程启动规则

线程的启动方法 start() 要发生在当前线程所有操作之前

线程终止规则

线程中所有的操作都要发生在线程终止之前,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见

线程中断规则

线程调用interrupt()方法要发生在被中断线程的代码检查出中断事件之前

对象终结规则

对象的初始化完成要发生在对象被回收之前

传递性规则

如果操作A发生在操作B之前,操作B又发生在操作C之前,那么操作A一定发生于操作C之前

注意:两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行,只需要前一个操作的结果对后一个操作可见,并且前一个操作按顺序要排在后一个操作之前。

6、数据依赖性

就是前一个操作的结果对后一个操作的结果产生影响,此时编译器和处理器在处理当前有数据依赖性的操作时不会改变存在数据依赖的两个操作的执行顺序

注意: 此时所说的数据依赖仅仅针对单个处理器中执行的指令序列或者单个线程中执行的操作。不同处理器和不同线程的情况编译器和处理器是不会考虑的

7、as-if-serial

在单线程情况下不管怎么重排序程序的执行结果不能被改变,所以如果在单处理器或者单线程的情况下,编译器和处理器对于有数据依赖性的操作是不会进行重排序的。反之如果没有数据依赖性的操作就有可能发生指令重排。

四、数据竞争与顺序一致性

在多线程情况下才会出现数据竞争

1、数据竞争

在一个线程中写了一个变量,在另一个线程中读一个变量,而且写和读并没有进行同步

2、顺序一致性

如果在多线程条件下,程序能够正确地使用同步机制,那么程序的执行将具有顺序一致性(就像在单线程条件下执行一样) 程序最终运行的结果与你预期的结果一样

3、顺序一致性内存模型

5.3.1特性:

一个线程中的所有操作必须按照程序的顺序来执行 所有的操作都必须是原子性的操作,并且对其他线程可见的

5.3.2概念:

在概念上,顺序一致性有一个单一的全局内存,在任意时间点最多只有一个线程可以连接到内存,当在多线程的场景下,会把所有内存的读写操作变成串行化

5.3.3案例:

例如有多个并发线程A B C, A 线程有两个操作A1 A2, 他们的执行的顺序是 A1->A2 。B 线程有三个操作B1 B2 B3, 他们的执行的顺序是B1->B2->B3 。C线程有两个操作C1 C2那么他们在程序中执行的顺序是C1->C2 。

场景分析:

场景一: 并发安全(同步)执行顺序

A1->A2->B1->B2->B3->C1->C2

场景二: 并发不安全(非同步)执行顺序

A1->B1->A2->C1->B2->B3->C2

结论:

在非同步的场景下,即使三个线程中的每一个操作乱序执行,但是在每个线程中的各自操作还是保持有序的。并且所有线程都只能看到一个一致的整体执行顺序,也就是说三个线程看到的都是该顺序 : A1->B1->A2->C1->B2->B3->C2 ,因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。

以上案例场景在JMM中不是这样的,未同步的程序在JMM中不仅整体的执行顺序变了,就连每个线程的看到的操作执行顺序也是不一样的。

例如前面所说的如果线程A将变量的值a=2写入到了自己的本地内存中,还没有刷入到主存中,在线程 A 来看值是变了,但是其他线程B线程C根本看不到值得改变,就认为线程A的操作还没有发生,只有线程A将工作内存中的值刷回主内存线程B和线程C才能的到。但是如果是同步的情况下,顺序一致性模型和JMM模型执行的结果是一致的,但是程序的执行顺序不一定,因为在JMM中,会发生指令重排现象所以执行顺序会不一致。

 

免责声明:

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

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

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

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

下载Word文档

猜你喜欢

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

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

深入理解JVM内存模型

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

一文带你深入了解JVM性能调优

Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。基本类型的变量保存原始值,即:他代表的值就是数值本身;而引用类型的变量保存引用值。“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。
JVM性能调优2024-12-03

深入理解并发编程艺术之JVM内存模型

Java语言无须任何同步手段保障就能成立的先行发生规则有且只有上面这些,下面演示一下如何使用这些规则去判定操作间是否具备顺序性,对于读写共享变量的操作来说,就是线程是否安全。

一步一图带你深入理解 Linux 物理内存管理

本文的目的是在深入理解虚拟内存管理的基础之上继续带大家向前奋进,一举击破物理内存管理的知识盲区,使大家能够俯瞰整个 Linux 内存管理子系统的整体全貌。

一文带你深入理解Golang中的泛型

Go 在泛型方面一直被诟病,因为它在这方面相对比较落后。但是,在 Go 1.18 版本中,泛型已经被正式引入,成为了 Go 语言中一个重要的特性。本文将会详细介绍 Go 泛型的相关概念,语法和用法,希望能够帮助大家更好地理解和应用这一特性
2023-05-18

一步一图带你深入理解 Linux 虚拟内存管理

本文我们从虚拟内存地址开始聊起,一直到物理内存地址结束,包含的信息量还是比较大的。首先笔者通过一个进程的运行实例为大家引出了内核引入虚拟内存空间的目的及其需要解决的问题。

一文带你深入理解GolangContext包

在Go语言中,Context包是一种非常常用的工具,它被用来管理goroutine之间的通信和取消。本文将深入探讨Context包的基本原理,包括使用场景、原理和一些最佳实践,需要的可以参考下
2023-05-18

一文带你深入理解Golang Context包

GolangContext包:深入理解GolangContext包为并发应用程序提供请求信息传递机制。它用于取消请求、传播元数据和跟踪请求范围。Context类型包含Value和Done方法,用于访问元数据和在取消时接收通知。创建Context的函数包括Background(),TODO(),WithCancel()和WithValue().使用Context将其作为函数参数传递,并使用Done()和Value()检查取消状态和获取元数据。最佳实践包括始终使用WithCancel()创建Context、使
一文带你深入理解Golang Context包
2024-04-23

怎么深入理解Java内存模型JMM

这期内容当中小编将会给大家带来有关怎么深入理解Java内存模型JMM,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。Java 内存模型Java 内存模型(JMM)是一种抽象的概念,并不真实存在,它描述了一组
2023-06-05

一文带你深入理解Golang中的RWMutex

这篇文章主要为大家详细介绍了Golang中RWMutex的相关知识,知其然,更要知其所以然。文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下
2023-05-14

深入理解 JVM 的内存区域划分

在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。
JVM内存区域2024-12-02

从 CPU 说起,深入理解 Java 内存模型!

这篇文章我们从底层 CPU 开始讲起,一直讲到操作系统,最后讲到了编程语言层面,让大家能够一环扣一环地理解,最后明白 Java 内存模型诞生的原因(上层有数据一致性问题),以及最终要解决的问题(缓存一致性问题)。

一文带你深入了解JavaScript中的原型&原型链

相信不少同学在面试的时候经常在基础上就挂掉了,当下行情实属严峻,如果我们基础都没有打牢固的话,实属有点面试浪费机会。本文就来和大家聊聊JavaScript中的原型&原型链,希望对大家有所帮助
2023-02-13

一文带你深入理解Vue3响应式原理

响应式就是当对象本身(对象的增删值)或者对象属性(重新赋值)发生变化时,将会运行一些函数,最常见的就是render函数,下面这篇文章主要给大家介绍了关于Vue3响应式原理的相关资料,需要的朋友可以参考下
2022-11-13

一篇带你深入理解Promise

我们都知道 JavaScript 的代码执行的时候是跑在单线程上的,可以理解为只能按照代码的出现顺序,从上到下一行一行的执行,但是遇到了异步的行为,比如定时器(一定时间之后才去执行),那就需要等同步代码执行完成后的一段时间里再去执行异步代码
Promise2024-12-03

编程热搜

  • 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动态编译

目录