Java 入门指南:Java 并发编程 —— JMM Java内存模型

news2024/12/27 13:55:53

JMM(Java Memory Model,Java 内存模型)(抽象模型)是用来描述和控制多线程之间内存可见性有序性原子性指令重排等问题的规范。

JMM 定义了一组规则,规定了在多线程环境下,线程在执行共享变量的读写操作时所应该遵守的顺序和限制,从而保证多线程之间的数据及时可见、有序和原子性。

![[JMM 模型架构.png]]
图片来源:悟空聊架构

线程的通信和同步问题

并发编程的线程之间存在两个问题:

  • 线程间如何通信——线程之间以何种机制来交换信息
  • 线程间如何同步——线程以何种机制来控制不同线程间发生的相对顺序

有两种并发模型可以解决这两个问题:
消息传递并发模型共享内存并发模型,Java 使用的是共享内存并发模型

![[不同模型下的线程的通信与同步.png]]

内存可见性

在栈中的变量(局部变量、方法定义的参数、异常处理的参数)不会在线程之间共享,也就不会有内存可见性的问题,也不受内存模型的影响。而在堆中的变量是共享的,一般称之为共享变量

线程之间的共享变量存在于主存中,每个线程都有一个私有的本地内存,存储了该线程的读、写共享变量的副本。本地内存是 Java 内存模型的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器等。

JMM 抽象模型

Java 线程之间的通信由 Java 内存模型(JMM)控制,从抽象的角度来说,JMM 定义了线程和主存之间的抽象关系。

![[Pasted image 20231212233758.png]]

  • 主内存属于共享数据区域,包含了堆和方法区

  • 本地内存属于私有数据区域,包含了程序计数器、本地方法栈、虚拟机栈

  1. 所有的共享变量都存在主存中。

  2. 每个线程都保存了一份该线程使用到的共享变量的副本。

  3. 如果线程 A 与线程 B 之间要通信的话,必须经历下面 2 个步骤:

    1. 线程 A 将本地内存 A 中更新过的共享变量刷新到主存中去。
    2. 线程 B 到主存中去读取线程 A 之前已经更新过的共享变量。

线程 A 无法直接访问线程 B 的工作内存,线程间通信必须经过主存。

根据 JMM 的规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主存中读取

内存架构

![[Pasted image 20231214161241.png]]
图片来源:悟空聊架构

  • 主内存:Java堆中对象实例数据部分,对应于物理硬件的内存

  • 工作内存:Java栈中的部分区域,优先存储于寄存器和高速缓存

线程 B 并不是直接去主存中读取共享变量的值,而是先在本地内存 B 中找到这个共享变量,发现这个共享变量已经被更新了,然后本地内存 B 去主存中读取这个共享变量的新值,并拷贝到本地内存 B 中,最后线程 B 再读取本地内存 B 中的新值。

JMM 与 重排序

指令重排序

指令重排序是现代 CPU 为了优化代码执行效率所做出的一种优化技术。重排序指编译器和 CPU 为了最大程度地提高程序运行效率,对指令的执行顺序进行的优化

优化方式
  1. 编译器优化:在不改变单线程程序语义的前提下,通过重新安排指令的执行顺序,来减少程序的耗时。

  2. CPU 优化:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),在执行指令的时间和底层硬件内部处理的时间不同时,通过使指令更好地利用处理器的性能,来减少程序的耗时。

指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致

由于现代 CPU 的并行能力非常强,因此对代码顺序的优化也非常敏感。这就可能出现指令重排序问题,也就是指在编译器和 CPU 的优化过程中,程序本来规定的指令执行顺序被改变,从而导致程序出现错误或者异常的情况。

为了避免指令重排序带来的问题,Java 语言提供了 volatile 关键字,它可以保证多线程操作共享变量的可见性、禁止指令重排序。此外,还可以使用 锁机制同步机制 来确保指令的正确执行顺序。

synchronized 不仅保证可见性,同时也保证了原子性(互斥性)

JMM 重排序

  • 重排序不会对存在数据依赖关系的操作进行重排序

    比如:a=1; b=a; 这个指令序列,因为第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。

  • 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。

    比如:a=1; b=2; c=a+b 这三个操作,第一步 (a=1) 和第二步 (b=2) 由于不存在数据依赖关系,所以可能会发生重排序,但是 c=a+b 这个操作是不会被重排序的,因为需要保证最终的结果一定是 c=a+b=3

内存屏障

在更底层,JMM 通过内存屏障来实现内存的可见性以及禁止重排序。

使用 volatile 关键字(volatile 关键字详解)来修饰一个变量时,Java 内存模型会插入内存屏障(一个处理器指令,可以对 CPU 或编译器重排序做出约束)来确保以下两点:

  • 写屏障(Write Barrier):当一个 volatile 变量被写入时,写屏障确保在该屏障之前的所有变量的写入操作都提交到主内存。

  • 读屏障(Read Barrier):当读取一个 volatile 变量时,读屏障确保在该屏障之后的所有读操作都从主内存中读取。

  • 当程序执行到 volatile 变量的读操作或者写操作时,在其前面操作的更改肯定已经全部进行,且结果对后面的操作可见;在其后面的操作肯定还没有进行;

  • 在进行指令优化时,不能将 volatile 变量的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。

执行到 volatile 变量时,其前面的所有语句都必须执行完,后面所有得语句都未执行。且前面语句的结果对 volatile 变量及其后面语句可见。

as-if-serial

as-if-serial 是一种编译器优化的概念,它允许编译器对程序进行重排序和优化,只要最终的执行结果和串行执行的结果一致即可。

具体来说,as-if-serial 的原则是,编译器和处理器可以对指令进行重排序和优化,只要保证程序最后的执行结果与串行执行的结果一致即可

在单线程情况下,编译器和处理器可以对指令进行并行执行、重排序或者删除一些无关紧要的指令,以提高程序的性能。

但是,as-if-serial 并不意味着编译器和处理器可以违反语言规范所定义的操作顺序或者程序的语义。它仅仅是允许优化的一种原则,其中的优化必须在不改变程序执行结果的前提下进行。

多线程环境下,as-if-serial 原则需要受到线程间的同步机制的限制,比如 volatile 关键字、锁、原子操作等,以保证线程间的协作和一致性。

happens-before

一方面,开发者需要 JMM 提供一个强大的内存模型来编写代码;另一方面,编译器和处理器希望 JMM 对它们的束缚越少越好,这样它们就可以尽可能多的做优化来提高性能,希望的是一个弱的内存模型。

JMM 考虑了这两种需求,并且找到了平衡点,对编译器和处理器来说,只要不改变程序的执行结果(单线程程序和正确同步了的多线程程序),编译器和处理器怎么优化都行。

设计者提出了 happens-before 的概念,更加简单易懂,从而避免了程序员为了理解内存可见性而去学习复杂的重排序规则,以及这些规则的具体实现方法。

happens-before(JSR-133规范) 是 Java 并发编程中的一个重要概念,用于描述同一或不同线程中两个操作之间的关系。如果 操作 A “happens-before” 操作 B,那么操作 A 在时间上已经发生,并且操作 A 的执行结果对操作 B 可见

在 Java 并发编程中,如果两个线程中的两个操作在 “happens-before” 的关系下,那么它们之间的执行顺序是确定的。Java 内存模型通过定义一组规则来确定操作之间的 “happens-before” 关系,以确保多线程程序的可见性和一致性,保证正确同步的多线程程序的执行结果不被重排序改变。

Java 并发编程中的 happens-before 关系包括以下几种情况:

  1. 程序顺序规则:在单个线程中,操作按照编写的顺序执行。

  2. volatile 变量规则:对一个 volatile 变量的写操作 happens-before 后续的读操作。

  3. 锁规则:释放锁的操作 happens-before 后续的获取同一个锁的操作。

  4. 传递性规则:如果 A happens-before B,B happens-before C,那么 A happens-before C。

  5. start() 规则:在一个线程内,start() 方法调用之前的操作 happens-before start() 方法之后的操作。

  6. join() 规则:在一个线程内,join() 方法之后的操作 happens-before join() 方法返回之前的操作。

顺序一致性

顺序一致性是并发编程中的一个重要概念,指的是程序在多线程环境下的执行结果与在顺序执行的理想情况下的执行结果相同

在顺序一致性模型中,所有的线程共享一个全局的执行序列,且所有操作按照它们在该序列中的顺序执行。在多线程并发执行的情况下,程序的执行结果必须与在单线程顺序执行的情况下一致。

顺序一致性要求对于每个线程来说,所有的操作都必须按照顺序一致的方式来执行,必须满足以下条件:

  1. 线程内部的操作必须按照程序的编写顺序执行(程序顺序一致性)。

  2. 不同线程之间的操作,如果它们之间存在数据依赖关系,那么这些操作必须按照顺序一致的方式相互配对执行。

  3. 无论程序是否同步,所有线程都只能看到一个单一的操作执行顺序。即在顺序一致性模型中,每个操作必须是原子性的,且立刻对所有线程可见

在实际的计算机系统中,为了提高性能和效率,可能会采用一些优化手段来改变操作的顺序,从而 违反了顺序一致性。为了解决这个问题,出现了弱一致性模型和内存模型,如 Java 内存模型(Java Memory Model, JMM),提供了更细粒度的内存访问控制和一致性保证。

JMM 不保证数据一致性

JMM 中,临界区内(同步块或同步方法中)的代码可以发生重排序(但不允许临界区内的代码“逃逸”到临界区之外,会破坏锁的内存语义)。

虽然线程 A 在临界区做了重排序,但是因为锁的特性,线程 B 无法观察到线程 A 在临界区的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。

同时,JMM 会在退出临界区和进入临界区做特殊的处理,使得在临界区内程序获得与顺序一致性模型相同的内存视图。

JMM 的具体实现方针是:在不改变(正确同步的)程序执行结果的前提下,尽量方便编译期和处理器的优化。

对于未同步的多线程,JMM 只提供最小安全性:线程读取到的值,要么是之前某个线程写入的值,要么是默认值,不会无中生有。

为了实现这个安全性,JVM 在堆上分配对象时,首先会对内存空间清零,然后才会在上面分配对象(这两个操作是同步的)。

如果要保证执行结果一致,那么 JMM 需要禁止大量的优化,对程序的执行性能会产生很大的影响。所以 JMM 没有保证未同步程序的执行结果与该程序在顺序一致性中执行结果一致。

非同步程序

未同步程序在 JMM 和顺序一致性内存模型中的执行特性有如下差异:

  1. 顺序一致性保证单线程内的操作会按程序的顺序执行

    JMM 不保证单线程内的操作会按程序的顺序执行。(因为重排序,但是 JMM 保证单线程下的重排序不影响执行结果)

  2. 顺序一致性模型保证所有线程只能看到一致的操作执行顺序

    JMM 不保证所有线程能看到一致的操作执行顺序。(因为 JMM 不保证所有操作立即可见)

  3. 顺序一致性模型保证对所有的内存读写操作都具有原子性

    JMM 不保证对 64 位的 longdouble 型变量的写操作具有原子性。

volatile

volatile,用于声明变量,用来修饰被不同线程访问和修改的共享变量。在 JVM 底层,volatile 是用内存屏障实现的。

观察汇编代码,对变量加入 volatile 关键字时,会多出一个 lock 前缀指令,lock 前缀指令实际上相当于一个内存屏障(也称内存栅栏)

应用场景和用法

  1. 修饰变量:可以使用 volatile 修饰变量,即 volatile 变量。被 volatile 修饰的变量具有可见性和禁止重排序的特性。

    volatile 修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值

    成员变量发生变化时,会强制线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。一个 volatile 对象引用可能是 null。

  2. 控制循环条件:当多个线程共同操作一个控制循环的标志变量时,可以使用 volatile 确保标志的及时可见性。

  3. 状态标记:当某个变量表示对象的状态,并且该状态可能被多个线程共享和修改时,使用 volatile 修饰该变量可以保证状态的一致性,确保共享变量的状态在不同线程间同步。

  4. 单例模式的实现:在双重检查锁定的单例模式中,可以使用 volatile 关键字修饰实例变量,确保实例的可见性和正确初始化。

volatile变量特性

  1. 可见性:被 volatile 修饰的变量对于所有线程都是可见的。当一个线程修改了 volatile 变量的值,其他线程可以立即看到最新的值,而不会使用过期的缓存副本。

  2. 禁止重排序:被 volatile 修饰的变量的读写操作具有禁止[[JMM Java内存模型#指令重排序|重排序]]的效果。它可以保证 volatile 变量的赋值操作不会被编译器重排序到其他内存操作的前面或后面。

  3. 轻量级同步机制:volatile 提供了一种轻量级的线程同步机制,避免了使用锁造成的线程切换和上下文切换的开销。

使用规范和注意事项

  1. 可见性要求:只有当变量的值可能会被多个线程同时访问并且其中一个线程对变量的写操作,而其他线程需要读取该变量的最新值时,才使用volatile 关键字。

    如果变量只会被单个线程访问,或者多个线程访问但只有某个线程对其进行写操作,那么 volatile 关键字是不必要的。

  2. 不具备原子性volatile 关键字只能确保对变量的读取和写入操作的可见性,但并不能保证复合操作的原子性。对于多线程并发更新的场景,需要使用其他具有原子性保证的机制

  3. 替代锁的使用:在某些场景下,volatile 可以作为一种轻量级的替代锁的机制,用于实现简单的线程同步。但对于复杂的同步需求,volatile不能保证访问变量的线程之间的互斥性

    例如,多个线程对一个 volatile 变量进行自增操作时,可能会发生竞态条件。这种情况下,应该使用其他同步机制(例如 Locksynchronized)来保证原子性。

    如果需要实现互斥访问,应该使用其他同步机制,如 synchronizedReentrantLock 等。

  4. 避免依赖于先前状态:当使用 volatile 关键字修饰变量时,应当避免依赖于变量的先前状态。因为多个线程之间无法保证操作的顺序,某个线程读取到的值可能并不是最新的。

    如果需要依赖于先前状态,那么 volatile 关键字可能并不适用,应该使用其他同步机制。

  5. 深入理解用法:使用 volatile 关键字需要深入理解其特性和适用场景,避免滥用。正确地使用 volatile 可以解决一些特定的并发问题

volatile的缺点

  1. 无法保证原子性:使用 volatile 修饰变量可以保证在多个线程之间的可见性,但不保证原子性。

    如果对于该变量的操作涉及到多个步骤,例如递增或递减操作,那么使用 volatile 修饰的变量可能无法保证原子性,可能会导致数据不一致。

  2. 有限的使用场景volatile 适用于一些简单的并发场景,例如作为标志位或开关,在多个线程之间共享状态信息。

    在复杂的并发操作中,如复合操作或多个变量之间的依赖关系,volatile 可能无法提供足够的保证。

  3. 降低性能:使用 volatile 修饰变量可能会导致一些性能开销。由于volatile 防止了编译器和处理器对变量的优化,可能会导致额外的内存访问和同步操作,从而降低性能。

  4. 可能引发缓存不一致问题:在多核处理器中,每个核心通常都有自己的缓存,使用 volatile 变量可能导致缓存不一致的问题。

    当一个线程修改了 volatile 变量的值,其他线程需要立即看到这个变化,但是缓存一致性协议(如MESI)可能需要时间来更新其他核心的缓存,这可能会导致一定的延迟。

  5. 不保证线程安全:使用 volatile 修饰变量可以确保可见性,但并不提供互斥性。如果多个线程对于同一个 volatile 变量进行写操作,可能会导致竞态条件和数据不一致性

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

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

相关文章

尚品汇-MQ模块搭建测试、消息不丢失(重)(四十三)

目录: (1)消息不丢失 (2)消息确认 (3)消息确认业务封装 (4)封装发送端消息确认 (5)封装消息发送 (6)发送确认消息测试…

【C#】Visual Studio 2017开发C#,按F1键没有跳转到C#帮助文档,反而跳到了Qt的帮助文档

1. 原因 Visual Studio中安装了Qt的插件,所以将F1的跳转链接转到了Qt的帮助文档。 2. F1改回微软帮助文档方法 工具 - 选项 - Qt - General - Try Qt Documentation when F1 is pressed改为Flase

Web服务端通过SSE推送消息给浏览器客户端的实现方案(附详细代码和仓库地址)

目录 1、SSE(Server-Sent Events)简介2、SSE 的工作原理3、SSE 与客户端轮询的区别和优势比较区别优势 4、SSE简单实现(单机应用Demo)演示效果SSE-Demo仓库地址下面直接贴代码:前端实现:后端实现: 5、SSE简单实现(分布…

【pycharm】汉化及翻译插件

汉化插件 翻译插件 使用 选中右键翻译

一键解决LBP2900通信错误的问题(同样支持Win 11系统)

**目录** **前言****常见解决方式****方案一:端口排除****方案二:服务重启****方案三:注册表注入修复** 前言 佳能LBP2900向来是经典耐用款的打印机。想必各位可能遇到过,由于老旧会出现奇葩的问题,譬如 就算USB接口已…

【C++篇】~类和对象(上)

【C篇】 类和对象上 一类二实例化内存对齐原因(用空间换时间,提高效率) 一类 ‘类’class可以理解为C语言阶段的‘结构体’,它的用法与struct大差不差很多地方都相同,但是C毕竟是C,类的用法肯定比结构体的…

Linux Kernel 6.12版预计将支持在崩溃后显示二维码 后续可以解码排查错误

7 月份时红帽工程师基于 systemd 255 版的全屏显示错误消息功能为 Linux Kernel 开发崩溃后显示二维码选项,这与微软在 Windows 10/11 蓝屏死机后显示二维码有异曲同工之妙。 不过 Linux 与 Windows 在崩溃时显示的二维码内容则有本质区别,因为 Window…

单链表反转(C语言)

1 问题描述 力扣(LeetCode)--反转链表 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 例如: 输入:head [1,2,3,4,5]输出:[5,4,3,2,1] 输入:head [1,2]输出&#x…

11 Java 方法引用、异常处理

文章目录 前言一、Java接口之函数式编程 --- 接口知识补充1 Function<T,R>泛型接口2 BiFunction<T, U, R>泛型接口3 自定义泛型函数式编程接口 二、方法引用1 方法引用初体验&#xff08;以Array.sort()方法为例&#xff09;2 引用静态方法3 引用其他类成员方法 前…

【面试五】PID控制算法

一、 PID算法简介 PID&#xff08;Proportional-Integral-Derivative&#xff09;控制算法是一种经典的反馈控制方法&#xff0c;广泛应用于自动控制系统&#xff0c;例如温度控制、速度控制、位置控制等。 PID控制算法的核心包含三个部分&#xff1a;比例项&#xff08;P&…

@antv/g6 业务场景:流程图

1、流程图是流经一个系统的信息流、观点流或部件流的图形代表。在企业中&#xff0c;流程图主要用来说明某一过程。这种过程既可以是生产线上的工艺流程&#xff0c;也可以是完成一项任务必需的管理过程。业务场景流程图如下&#xff1a; 2、绘制流程图的 Tips 流程图一般是用…

计算机毕业设计选题推荐-果树生长信息管理系统-Java/Python项目实战

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

Redis三种集群模式:主从模式、哨兵模式和Cluster模式

1. 总结经验 redis主从&#xff1a;可实现高并发(读)&#xff0c;典型部署方案&#xff1a;一主二从 redis哨兵&#xff1a;可实现高可用&#xff0c;典型部署方案&#xff1a;一主二从三哨兵 redis集群&#xff1a;可同时支持高可用(读与写)、高并发&#xff0c;典型部署方…

探索Python数据持久化的秘密:ZODB库的神奇之旅

文章目录 探索Python数据持久化的秘密&#xff1a;ZODB库的神奇之旅背景ZODB是什么&#xff1f;如何安装ZODB&#xff1f;简单库函数使用方法场景应用常见Bug及解决方案总结 探索Python数据持久化的秘密&#xff1a;ZODB库的神奇之旅 背景 在Python的广阔世界中&#xff0c;数…

基于单片机的水箱水质监测系统设计

本设计基于STM32F103C8T6为核心控制器设计了水质监测系统&#xff0c;选用DS18B20温度传感器对水箱水体温度进行采集&#xff1b;E-201-C PH传感器获取水体PH值&#xff1b;选用TS-300B浊度传感器检测水体浊度&#xff1b;采用YW01液位传感器获取水位&#xff0c;当检测水位低于…

网络压缩之知识蒸馏(knowledge distillation)

因为直接训练一个小的网络&#xff0c;往往结果就是没有从大的网络剪枝好。知识蒸馏的概念是 一样的&#xff0c;因为直接训练一个小的网络&#xff0c;没有小的网络根据大的网络来学习结果要来得 好。 因而&#xff0c;先训练一个 大的网络&#xff0c;这个大的网络在知识蒸馏…

Flutter 初识:Chip控件

Flutter Chip控件小结 Chip属性解析示例 InputChip属性解析示例 ChoiceChip属性解析示例 FilterChip属性解析示例 ActionChip属性解析示例 在 Flutter 中&#xff0c;Chip 是一种用于显示简洁信息的组件。它通常用来展示标签、属性、短的文本片段等&#xff0c;并可以包含可选的…

C语言推箱子迷宫

目录 开头程序程序的流程图程序游玩的效果下一篇博客要说的东西 开头 大家好&#xff0c;我叫这是我58。 程序 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <Windows.h> typedef stru…

python内置模块datetime.date类详细介绍

Python的datetime模块是一个强大的日期和时间处理库&#xff0c;它提供了多个类来处理日期和时间。主要包括几个功能类datetime.date、datetime.time、datetime.datetime、datetime.timedelta,datetime.timezone等。 使用datetime模块 要使用 datetime模块&#xff0c;直接导…

大模型辅助软件开发,助力工程师的开发之路

大模型与软件工程师&#xff1a;改变开发范式的力量 “是人类工程师的能力&#xff0c;而不是大模型的能力&#xff0c;决定了大模型协作式开发的上限。” 这句话深刻地揭示了在人工智能&#xff0c;尤其是大型语言模型&#xff08;LLM&#xff09;飞速发展的今天&#xff0c…