Java Memory Model

内存模型

JMM(Java 内存模型)主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。

Java Memory Model(JMM),本身是一种抽象的概念,实际上并不存在,描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。

在并发编程下,像 CPU 多级缓存指令重排这类设计可能会导致程序运行出现一些问题。就比如说指令重排序就可能会让多线程程序的执行出现问题,为此,JMM 抽象了 happens-before 原则来解决这个指令重排序问题。

JMM 作用

  • 屏蔽各种硬件和操作系统的内存访问差异,实现让 Java 程序在各种平台下都能达到一致的内存访问效果
  • 规定了线程和内存之间的一些关系

Java 内存模型(JMM) 抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中

Java 中所有变量都存储在主存中,对于所有线程都是共享的;每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成。

本地内存只是JMM的抽象,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化

内存交互 

Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作,每个操作都是原子

  • lock:作用于主内存,将一个变量标识为被一个线程独占状态(对应 monitorenter)
  • unclock:作用于主内存,将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定(对应 monitorexit)
  • read:作用于主内存,把一个变量的值从主内存传输到工作内存中
  • load:作用于工作内存,在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
  • use:作用于工作内存,把工作内存中一个变量的值传递给执行引擎,每当遇到一个使用到变量的操作时都要使用该指令
  • assign:作用于工作内存,把从执行引擎接收到的一个值赋给工作内存的变量
  • store:作用于工作内存,把工作内存的一个变量的值传送到主内存中
  • write:作用于主内存,在 store 之后执行,把 store 得到的值放入主内存的变量中

JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证

 除了这 8 种同步操作之外,还规定了下面这些同步规则来保证这些同步操作的正确执行

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

内存模型三大特性

原子性

定义原子操作的使用规则:

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

可见性

可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。

主要有三种实现可见性的方式:

  • volatile
  • synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
  • final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。

有序性

有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

CPU 的基本工作是执行存储的指令序列,即程序,程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程,为了提高性能,编译器和处理器会对指令重排,一般分为以下三种:

源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令

int num = 0;
boolean ready = false;// 线程1 执行此方法
public void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;}}// 线程2 执行此方法
public void actor2(I_Result r) {num = 2;ready = true;
}

I_Result 是一个对象,有一个属性 r1 用来保存结果

可能的结果有几种?

情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1

情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1

情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了) 

情况4:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2

这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化

JMM中提供一下方式来保证有序性:

  • happens-before原则:happens-before原则是java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,也就是说发生操作B之前,操作A产生的影响能被操作B观察到。这里的“影响”包括修改共享变量,方法调用。
  • volatile机制:volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
  • synchronized机制:synchronized能够保证有序性是因为synchronized可以保证同一时间只有一个线程访问代码块,而单线程环境下,JMM能够保证代码的串行语义;虽然使用synchronized的代码块,还可以发生指令重排序,但是synchronized可以保证只有一个线程执行,所以最后的结果还是正确的。

总结JMM对特性提供的支持如下:

特性

volatile关键字

synchronized关键字

Lock接口

Atomic变量

原子性

无法保障

可以保障

可以保障

可以保障

可见性

可以保障

可以保障

可以保障

可以保障

有序性

一定程度

可以保障

可以保障

无法保障

happens-before 原则

为了提高处理速度,JVM会对代码进行编译优化,也就是指令重排序优化,并发编程下指令重排序会带来一些安全隐患:如指令重排序导致的多个线程操作之间的不可见性。

如果让程序员再去了解这些底层的实现以及具体规则,那么程序员的负担就太重了,严重影响了并发编程的效率。

从JDK5开始,提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。

JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内存可见性。

happens-before 原则的诞生是为了程序员和编译器、处理器之间的平衡。程序员追求的是易于理解和编程的强内存模型,遵守既定规则编码即可。编译器和处理器追求的是较少约束的弱内存模型,让它们尽己所能地去优化性能,让性能最大化。happens-before 原则的设计思想其实非常简单:

  • 为了对编译器和处理器的约束尽可能少,只要不改变程序的执行结果(单线程程序和正确执行的多线程程序),编译器和处理器怎么进行重排序优化都行。
  • 对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序。

Java 内存模型具有一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从 happens-before 原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

 JMM 设计思想的示意图

了解了 happens-before 原则的设计思想,我们再来看看 JSR-133 对 happens-before 原则的定义:

  • 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,并且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么 JMM 也允许这样的重排序。

单一线程原则:一个线程内,按照代码顺序,书写在前面的操作 happens-before 于书写在后面的操作

管程锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作

volatile 变量规则:对一个 volatile 变量的写操作 happens-before 于后面对这个 volatile 变量的读操作。说白了就是对 volatile 变量的写操作的结果对于发生于其后的任何操作都是可见的。

线程启动规则:Thread 对象的 start()方法 happens-before 于此线程的每一个动作

线程加入规则:Thread 对象的结束先行发生于 join() 方法返回

线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。

对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。

传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C

如果两个操作不满足上述任意一个 happens-before 规则,那么这两个操作就没有顺序的保障,JVM 可以对这两个操作进行重排序。

private int value = 0;public void setValue(int value){this.value = value;
}public int getValue(){return value;
}

假设两个线程A和B,线程A先(在时间上先)调用了这个对象的setValue(1),接着线程B调用了getValue()方法,那么B的返回值是多少?

对照 happens-before 原则,上面的操作不满下面的条件:

  • 不是同一个线程,所以不涉及:程序顺序规则。
  • 不涉及同步,所以不涉及:监视器锁规则。
  • 没有volatile,所以不涉及:volatile变量规则。
  • 没有线程的启动和中断,所以不涉及:start()规则,join规则,程序中断规则。
  • 没有对象的创建和终结,所以不涉及:finalizer规则。
  • 更没有传递规则。

所以,一条规则都不满足,尽管线程A在时间上与线程B具有先后顺序,但是,却不满足happens-before原则,也就是有序性并不会保障,所以线程B获取到的数据是不安全的!!!这也反向说明了happens-before原则提到的关系和时间的先后顺序没有关系。

时间先后顺序与先行发生原则之间基本没有太大关系,所以我们衡量并发安全问题的时候不要收到时间顺序的干扰,一切必须以先行发生原则为准。只有真正满足了happens-before原则,才能保证安全

happens-before 与 JMM 的关系

内存屏障

内存屏障(Memory Barrier),也称为内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以执行此点之后的操作。大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。

语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。

内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个:

  1. 一是保证特定操作的执行顺序;
  2. 二是保证某些变量的内存可见性(利用该特性实现 volatile 的内存可见性)

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条内存屏障则会告诉编译器和CPU,不管什么指令都不能和这条内存屏障指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。


内存屏障的另外一个作用是强制刷出各种 CPU 的缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。

CPU层面的内存屏障分为三类:

写屏障(Store Memory Barrier):告诉处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对写屏障之后的读或者写是可见的。

读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的。

全屏障(Full Memory Barrier):确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作。

在JMM中将内存屏障分为四类:LoadLoad Barrier;StoreStore Barrier;LoadStore Barrier;StoreLoad Barrier

volatile

volatile 是Java虚拟机提供的轻量级的同步机制。volatile 关键字有如下两个作用:

  1. 保证被 volatile 修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了被 volatile 修饰共享变量的值,新值总是可以被其他线程立即得知
  2. 禁止指令重排序优化

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  • 它会强制将对缓存的修改操作立即写入主存;
  • 如果是写操作,它会导致其他CPU中对应的缓存行无效。

Lock 不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock 会对 CPU总线和高速缓存加锁,可以理解为 CPU 指令级的一种锁。

下面是JMM针对编译器制定的 volatile 重排序规则表

为了实现 volatile 的内存语义,编译在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM 采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。

  • 在每个volatile写操作的前面插入一个StoreStore屏障;
  • 在每个volatile写操作的后面插入一个StoreLoad屏障;
  • 在每个volatile读操作的后面插入一个LoadLoad屏障;
  • 在每个volatile读操作的后面插入一个LoadStore屏障;

volatile 写插入内存屏障后生成的指令序列示意图

volatile 读插入内存屏障后生成的指令序列示意图

 

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

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

相关文章

头戴耳机最值得购买的牌子有哪些?四款公认口碑和性价比高推荐

说到头戴式耳机,大部分人都会面另一个问题,那就是如何选购一款好用实惠的头戴耳机。有的人图便宜,结果不仅音质不好,佩戴还不舒服,而有的人图牌子,结果却交了“智商税”!俗话说买对不买贵&#…

xx.sh: 行 2: $‘\r‘:未找到命令

一、解决方案 先说解决方案:使用 dos2unix 工具将文件的换行符转换为 Unix 格式 sudo apt install dos2unix dos2unix scripts/v1_5/eval/textvqa.sh二、具体问题 再分析具体问题 在运行本地的一个sh文件时,出现无法找到命令的报错 bash scripts/v1…

2015年-2016年 软件工程程序设计题(算法题)实战_c语言程序设计数据结构程序设计分析

文章目录 2015年1.c语言程序设计部分2.数据结构程序设计部分 2016年1.c语言程序设计部分2.数据结构程序设计部分 2015年 1.c语言程序设计部分 1.从一组数据中选择最大的和最小的输出。 void print_maxandmin(double a[],int length) //在一组数据中选择最大的或者最小的输出…

JavaWeb合集08-项目开发实战

八、项目开发 1、项目搭建 1.1 配置配置文件 application.properties文件 spring.application.namemybatis_testspring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver spring.datasource.urljdbc:mysql://localhost:3306/homepage spring.datasource.usernameroot sp…

闪迪sd卡如何恢复删除的内容?这3种方法很实用

在数字时代,闪迪SD卡因其便携性、高容量和稳定性,成为我们日常生活中不可或缺的数据存储设备。然而,数据丢失的风险也如影随形,一个不小心的误删除、格式化或是其他意外情况,都可能让我们珍贵的照片、视频、文档等重要…

Vue详细入门(语法【一】)

今天滴学习目标!!! Vue1. 什么是Vue.js2. Vue是怎么使用的嘞3. MVVM是什么嘞?4. Vue有哪些指令1. v-bind2.v-model3.v-if 和 v-else4. v-for5. v-show6. v-on 咱们先了解一下什么是Vue,Vue是怎么使用的嘞,V…

计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-19

计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-19 目录 文章目录 计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-19目录1. What makes your model a low-empathy or warmth person: Exploring theOrigins of Personality in LLMs摘要研究背景算…

小公司团队管理:8个你需要知道的要点

小公司要想管理好团队,关键在于明确的目标与愿景、有效的沟通机制、灵活的激励制度、培养团队协作精神、领导者以身作则。其中,有效的沟通机制尤为重要。建立畅通的沟通渠道,不仅可以提高工作效率,还能及时发现并解决问题&#xf…

MatLab Desired port was :31515解决方案

前言:使用的MatLabR2022b今天突然出现了错误,在程序中打不开文件。后尝试了下面的方法,可以解决。 解决方法一: 搜索栏输入:firewall.cpl 找到相关项,右键属性,设置为允许。 之后就可以了…

【个人同步与备份】电脑(Windows)与手机/平板(Android)之间文件同步

文章目录 1. syncthing软件下载2. syncthing的使用2.1. 添加设备2.1.1. syncthing具备设备发现功能,因此安装好软件,只需确认设备信息是否对应即可2.1.2. 如果没有发现到,可以通过设备ID连接2.1.3. 设置GUI身份验证用户,让无关设备…

FloodFill 算法(DFS)

文章目录 FloodFill 算法(DFS)图像渲染岛屿数量岛屿的最大面积被围绕的区域太平洋大西洋水流问题扫雷游戏衣橱整理 FloodFill 算法(DFS) 漫水填充(Flood Fi)算法是一种图像处理算法,在计算机图形学和计算机视觉中被广泛…

阿里开源Multi-(llm)Agent应用框架: Agentscope探究与学习

1. 架构图 2. 核心概念 Core Concepts 2.1 Message(消息) 是信息的载体(例如指令、多模态数据和对话内容)。在AgentScope中,消息是Python字典的子类, 具有name和content作为必要字段,url作为可…

[项目详解][boost搜索引擎#1] 概述 | 去标签 | 数据清洗 | scp

目录 一、前言 二、项目的相关背景 三、搜索引擎的宏观原理 四、搜索引擎技术栈和项目环境 五、正排索引 VS 倒排索引--原理 正排索引 分词 倒排索引 六、编写数据去除标签和数据清洗模块 Parser 1.数据准备 parser 编码 1.枚举文件 EnumFile 2.去标签ParseHtml(…

【CSS in Depth 2 精译_048】7.2 CSS 响应式设计中的媒体查询原则(中):页面断点(breakpoint)样式的添加

当前内容所在位置(可进入专栏查看其他译好的章节内容) 【第七章 响应式设计】(概述) 7.1 移动端优先设计原则(上篇) 7.1.1 创建移动端菜单(下篇)7.1.2 给视口添加 meta 标签&#xf…

「数学::快速幂」矩阵快速幂运算|快速斐波那契数列 / LeetCode 509(C++)

目录 概述 思路 算法过程 复杂度 Code 概述 LeeCode 509: 斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) 0&#xff…

linux--库指令

ldd ldd 可执行文件路径 显示依赖的库的查找路径以及是否查找到了。

Request2:Post请求和Json

百度翻译拿到自己想看的数据,下图查看请求到数据的请求 preview提前看下 取出对应的RequestUrl ,看出来要使用的话得用post请求 #!/usr/bin/env python # -*- coding:utf-8 -*- import requests import json if __name__ "__main__":#1.指定…

SAP物料凭证报表字段调整

业务场景: 报表MB51的输入和输出字段调整: 输入(选择界面) 输出界面 可以看到在这是没有布局调整的 后台路径: SPRO-物料管理-库存管理和实际库存-报表-定义物料凭证列表的字段选择 事务码:SM30-V_MMI…

FPGA学习(6)-基础语法参数化设计阻塞与非阻塞

目录 1.两种参数化不改变源文件,只改仿真文件的值 2.参数化设计实现模块的重用 2.1不用参数化方法 2.1.1源文件 2.1.2仿真文件 2.1.3仿真波形及实验 2.2 用参数方法 2.2.1调用之前写的led灯闪烁模块,在本源函数中,例化4次调用之前的模…

史上最烂 spring transaction 原理分析

史上最烂 spring transaction 原理分析 事务定义、事务作用、事务特性、生命周期、数据库事务三种运行模式、数据库事务控制、并发事务问题、隔离级别、数据库事务实现原理、spring 事务传播行为、spring 事务核心组件、spring boot 事务相关组件、事务嵌套原理、编程式事务与声…