JVM—内存模型(JMM)

之前字节面试问到过Java内存模型,回答的是内存结构。重新学习了一下整理出来。

1、前置知识

通信是指线程之间如何交换信息,主要有两种机制:共享内存 和 **消息传递,**共享内存指的是多个线程共享的数据区,A 线程写,B 线程读,这就是一次简单的线程通信;在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。

同步是指程序控制不同线程之间操作发生相对顺序的机制。

  • 在共享内存并发模型里,同步是显示进行的。
  • 在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

2、Java 内存模型的抽象☆

在 Java 中,所有实例域、静态域、和数组元素存储在堆内存中,堆内存存在线程之前共享。局部变量,方法定义参数和异常处理器参数不会在线程之间共享,它们不会有可见性的问题,就是不受内存模型的影响

Java 线程之间的通信由 Java 内存模型(JMM)控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见

JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每一个线程都有一个私有的本地内存,本地内存中存储了该线程以读 / 写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在。它包含了缓存、写缓存区、寄存器以及其他的硬件和编译器优化。

Java 内存模型的抽象示意图

从上图来看,线程 A 与线程 B 之间如要通信的话,必须要经历下面 2 个步骤:

  • 首先,线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中去。
  • 然后,线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。

通过示意图来说明这两个步骤:

如上图所示,本地内存 A 和 B 有主内存中共享变量 x 的副本。假设初始时,这三个内存中的 x 值都为 0。线程 A 在执行时,把更新后的 x 值(假设值为 1)临时存放在自己的本地内存 A 中。当线程 A 和线程 B 需要通信时,线程 A 首先会把自己本地内存中修改后的 x 值刷新到主内存中,此时主内存中的 x 值变为了 1。随后,线程 B 到主内存中去读取线程 A 更新后的 x 值,此时线程 B 的本地内存的 x 值也变为了 1。

从整体来看,这两个步骤实质上是线程 A 在向线程 B 发送消息,而且这个通信过程必须要经过主内存。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 java 程序员提供内存可见性保证

3、重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

4、happen-before 规则

基于重排序既能提升程序性能,另一外面会导致程序出问题,我们既要保留它的优点还要禁止出现问题的重排序指令,这个事情交给 JMM 来处理。

JMM 定义了一套规则,叫做 happen-before 规则,这套规则一方面给程序员的要求,一方面是对编译器和处理器的约束。JMM 承诺程序员基于这套编程规则,即便不理解重排序,程序也不会因为发生了重排序出问题。

本质目的就是:保留不打破规则的重排序,禁止打破规则的重排序

happen-before 规则(文心一言生成)

在Java中,happen-before规则是一组定义了操作执行顺序的规则,用于解决并发问题。这些规则确保了线程之间的可见性和有序性。以下是happen-before规则的详细内容:

  1. 程序次序原则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。这是最基础的规则,它确定了在一个线程内部的执行顺序。
  2. 监视器锁规则:对一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须指同一个锁,后面指的是时间上的先后顺序。这个规则确保了对共享资源的同步访问,防止数据竞争。
  3. volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。这里的后面同样指时间上的先后顺序。volatile关键字用于确保变量的读写操作具有原子性,通过内存屏障来实现。
  4. 传递性原则:如果A操作happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。这是传递性原则,确保了操作之间的顺序关系具有传递性。
  5. 线程启动规则:同一个线程的start()方法happen-before此线程的其它方法。这意味着线程的启动顺序决定了方法的执行顺序。
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的检测到中断时间的发生。这个规则确保了线程中断的正确处理。
  7. 线程终结规则:线程中的所有操作都happen-before线程的终止检测。这个规则确保了在线程结束之前,所有操作都已经完成。
  8. 对象创建规则:一个对象的初始化完成先行发生于他的finalize()方法调用。这个规则确保了在对象被回收之前,初始化已经完成。

这些happen-before规则为Java程序员提供了一种方式来理解和预测并发程序的行为,从而编写正确的并发代码。它们有助于确保操作的可见性和有序性,从而避免了数据不一致和其他并发问题。

相关推荐
努力的家伙是不讨厌的几秒前
解析json导出csv或者直接入库
开发语言·python·json
Envyᥫᩣ14 分钟前
C#语言:从入门到精通
开发语言·c#
九圣残炎31 分钟前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
wclass-zhengge33 分钟前
Netty篇(入门编程)
java·linux·服务器
童先生35 分钟前
Go 项目中实现类似 Java Shiro 的权限控制中间件?
开发语言·go
lulu_gh_yu36 分钟前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法
Re.不晚1 小时前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
老秦包你会1 小时前
Qt第三课 ----------容器类控件
开发语言·qt
凤枭香1 小时前
Python OpenCV 傅里叶变换
开发语言·图像处理·python·opencv
雷神乐乐1 小时前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven