什么是Java内存模型?

article/2025/10/5 16:48:02

点击上方“程序员小灰”,选择“置顶公众号”

有趣有内涵的文章第一时间送达!



本文转载自公众号 占小狼的博客


说”JVM内存模型“,有人会说是关于JVM内存分布(堆栈,方法区等)这些介绍,也有地方说(深入理解JVM虚拟机)上说Java内存模型是JVM的抽象模型(主内存,本地内存)。这两个到底怎么区分啊?有必然关系吗?比如主内存就是堆,本地内存就是栈,这种说法对吗?

时间久了,我也把内存模型和内存结构给搞混了,所以抽了时间把JSR133规范中关于内存模型的部分重新看了下。

后来听了好多人反馈:在面试的时候,有面试官会让你解释一下Java的内存模型,有些人解释对了,结果面试官说不对,应该是堆啊、栈啊、方法区什么的(这不是半吊子面试么,自己概念都不清楚)

JVM中的堆啊、栈啊、方法区什么的,是Java虚拟机的内存结构,Java程序启动后,会初始化这些内存的数据。

内存结构就是上图中内存空间这些东西,而Java内存模型,完全是另外的一个东西。

什么是内存模型

在多CPU的系统中,每个CPU都有多级缓存,一般分为L1、L2、L3缓存,因为这些缓存的存在,提供了数据的访问性能,也减轻了数据总线上数据传输的压力,同时也带来了很多新的挑战,比如两个CPU同时去操作同一个内存地址,会发生什么?在什么条件下,它们可以看到相同的结果?这些都是需要解决的。

所以在CPU的层面,内存模型定义了一个充分必要条件,保证其它CPU的写入动作对该CPU是可见的,而且该CPU的写入动作对其它CPU也是可见的,那这种可见性,应该如何实现呢?

有些处理器提供了强内存模型,所有CPU在任何时候都能看到内存中任意位置相同的值,这种完全是硬件提供的支持。

其它处理器,提供了弱内存模型,需要执行一些特殊指令(就是经常看到或者听到的,memory barriers内存屏障),刷新CPU缓存的数据到内存中,保证这个写操作能够被其它CPU可见,或者将CPU缓存的数据设置为无效状态,保证其它CPU的写操作对本CPU可见。通常这些内存屏障的行为由底层实现,对于上层语言的程序员来说是透明的(不需要太关心具体的内存屏障如何实现)。

前面说到的内存屏障,除了实现CPU之前的数据可见性之外,还有一个重要的职责,可以禁止指令的重排序。

这里说的重排序可以发生在好几个地方:编译器、运行时、JIT等,比如编译器会觉得把一个变量的写操作放在最后会更有效率,编译后,这个指令就在最后了(前提是只要不改变程序的语义,编译器、执行器就可以这样自由的随意优化),一旦编译器对某个变量的写操作进行优化(放到最后),那么在执行之前,另一个线程将不会看到这个执行结果。

当然了,写入动作可能被移到后面,那也有可能被挪到了前面,这样的“优化”有什么影响呢?这种情况下,其它线程可能会在程序实现“发生”之前,看到这个写入动作(这里怎么理解,指令已经执行了,但是在代码层面还没执行到)。通过内存屏障的功能,我们可以禁止一些不必要、或者会带来负面影响的重排序优化,在内存模型的范围内,实现更高的性能,同时保证程序的正确性。

下面看一个重排序的例子:

  
  1. Class Reordering {

  2.  int x = 0, y = 0;

  3.  public void writer() {

  4.    x = 1;

  5.    y = 2;

  6.  }

  7.  public void reader() {

  8.    int r1 = y;

  9.    int r2 = x;

  10.  }

  11. }

假设这段代码有2个线程并发执行,线程A执行writer方法,线程B执行reader方法,线程B看到y的值为2,因为把y设置成2发生在变量x的写入之后(代码层面),所以能断定线程B这时看到的x就是1吗?

当然不行! 因为在writer方法中,可能发生了重排序,y的写入动作可能发在x写入之前,这种情况下,线程B就有可能看到x的值还是0。

在Java内存模型中,描述了在多线程代码中,哪些行为是正确的、合法的,以及多线程之间如何进行通信,代码中变量的读写行为如何反应到内存、CPU缓存的底层细节。

在Java中包含了几个关键字:volatile、final和synchronized,帮助程序员把代码中的并发需求描述给编译器。Java内存模型中定义了它们的行为,确保正确同步的Java代码在所有的处理器架构上都能正确执行。

### synchronization 可以实现什么 Synchronization有多种语义,其中最容易理解的是互斥,对于一个monitor对象,只能够被一个线程持有,意味着一旦有线程进入了同步代码块,那么其它线程就不能进入直到第一个进入的线程退出代码块(这因为都能理解)。

但是更多的时候,使用synchronization并非单单互斥功能,Synchronization保证了线程在同步块之前或者期间写入动作,对于后续进入该代码块的线程是可见的(又是可见性,不过这里需要注意是对同一个monitor对象而言)。在一个线程退出同步块时,线程释放monitor对象,它的作用是把CPU缓存数据(本地缓存数据)刷新到主内存中,从而实现该线程的行为可以被其它线程看到。在其它线程进入到该代码块时,需要获得monitor对象,它在作用是使CPU缓存失效,从而使变量从主内存中重新加载,然后就可以看到之前线程对该变量的修改。

但从缓存的角度看,似乎这个问题只会影响多处理器的机器,对于单核来说没什么问题,但是别忘了,它还有一个语义是禁止指令的重排序,对于编译器来说,同步块中的代码不会移动到获取和释放monitor外面。

下面这种代码,千万不要写,会让人笑掉大牙:

  
  1. synchronized (new Object()) {

  2. }

这实际上是没有操作的操作,编译器完成可以删除这个同步语义,因为编译知道没有其它线程会在同一个monitor对象上同步。

所以,请注意:对于两个线程来说,在相同的monitor对象上同步是很重要的,以便正确的设置happens-before关系。

final 可以影响什么

如果一个类包含final字段,且在构造函数中初始化,那么正确的构造一个对象后,final字段被设置后对于其它线程是可见的。

这里所说的正确构造对象,意思是在对象的构造过程中,不允许对该对象进行引用,不然的话,可能存在其它线程在对象还没构造完成时就对该对象进行访问,造成不必要的麻烦。

  
  1. class FinalFieldExample {

  2.  final int x;

  3.  int y;

  4.  static FinalFieldExample f;

  5.  public FinalFieldExample() {

  6.    x = 3;

  7.    y = 4;

  8.  }

  9.  static void writer() {

  10.    f = new FinalFieldExample();

  11.  }

  12.  static void reader() {

  13.    if (f != null) {

  14.      int i = f.x;

  15.      int j = f.y;

  16.    }

  17.  }

  18. }

上面这个例子描述了应该如何使用final字段,一个线程A执行reader方法,如果f已经在线程B初始化好,那么可以确保线程A看到x值是3,因为它是final修饰的,而不能确保看到y的值是4。 如果构造函数是下面这样的:

  
  1. public FinalFieldExample() { // bad!

  2.  x = 3;

  3.  y = 4;

  4.  // bad construction - allowing this to escape

  5.  global.obj = this;

  6. }

这样通过global.obj拿到对象后,并不能保证x的值是3.

volatile可以做什么

Volatile字段主要用于线程之间进行通信,volatile字段的每次读行为都能看到其它线程最后一次对该字段的写行为,通过它就可以避免拿到缓存中陈旧数据。它们必须保证在被写入之后,会被刷新到主内存中,这样就可以立即对其它线程可以见。类似的,在读取volatile字段之前,缓存必须是无效的,以保证每次拿到的都是主内存的值,都是最新的值。volatile的内存语义和sychronize获取和释放monitor的实现目的是差不多的。

对于重新排序,volatile也有额外的限制。

下面看一个例子:

  
  1. class VolatileExample {

  2.  int x = 0;

  3.  volatile boolean v = false;

  4.  public void writer() {

  5.    x = 42;

  6.    v = true;

  7.  }

  8.  public void reader() {

  9.    if (v == true) {

  10.      //uses x - guaranteed to see 42.

  11.    }

  12.  }

  13. }

同样的,假设一个线程A执行writer,另一个线程B执行reader,writer中对变量v的写入把x的写入也刷新到主内存中。reader方法中会从主内存重新获取v的值,所以如果线程B看到v的值为true,就能保证拿到的x是42.(因为把x设置成42发生在把v设置成true之前,volatile禁止这两个写入行为的重排序)。

如果变量v不是volatile,那么以上的描述就不成立了,因为执行顺序可能是v=true, x=42,或者对于线程B来说,根本看不到v被设置成了true。

double-checked locking的问题

臭名昭著的双重检查(其中一种单例模式),是一种延迟初始化的实现技巧,避免了同步的开销,因为在早期的JVM,同步操作性能很差,所以才出现了这样的小技巧。

  
  1. private static Something instance = null;

  2. public Something getInstance() {

  3.  if (instance == null) {

  4.    synchronized (this) {

  5.      if (instance == null)

  6.        instance = new Something();

  7.    }

  8.  }

  9.  return instance;

  10. }

这个技巧看起来很聪明,避免了同步的开销,但是有一个问题,它可能不起作用,为什么呢?因为实例的初始化和实例字段的写入可能被编译器重排序,这样就可能返回部门构造的对象,结果就是读到了一个未初始化完成的对象。

当然,这种bug可以通过使用volatile修饰instance字段进行fix,但是我觉得这种代码格式实在太丑陋了,如果真要延迟初始化实例,不妨使用下面这种方式:

  
  1. private static class LazySomethingHolder {

  2.  public static Something something = new Something();

  3. }

  4. public static Something getInstance() {

  5.  return LazySomethingHolder.something;

  6. }

由于是静态字段的初始化,可以确保对访问该类的所以线程都是可见的。

对于这些,我们需要关心什么

并发产生的bug非常难以调试,通常在测试代码中难以复现,当系统负载上来之后,一旦发生,又很难去捕捉,为了确保程序能够在任意环境正确的执行,最好是提前花点时间好好思考,虽然很难,但还是比调试一个线上bug来得容易的多。



—————END—————




喜欢本文的朋友们,欢迎长按下图关注订阅号程序员小灰,收看更多精彩内容



http://chatgpt.dhexx.cn/article/YVuMTLB4.shtml

相关文章

Java面试--Java内存模型

面试题:你了解Java内存模型吗?(顺丰面试题) 面试题:程序内存的分布,五个部分(360面试题) 一、Java程序的执行过程: Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中…

JAVA内存模型与JVM内存模型

JAVA内存模型(JMM)与JVM内存模型 1、JAVA内存模型保证了在多线程环境下,对共享变量读写的原子性、可见性和有序性的一系列规范。 2、JVM内存模型规定了JAVA虚拟机在运行时使用的内存的各个分区及其作用。JAVA内存模型 原子性: 通过 synchronized、lock实现&#x…

Java内存模型(JMM)详解

目录 一、为什么要有内存模型二、CPU和缓存一致性2.1 为什么需要CPU cache2.2 三级缓存(L1、L2、L3)2.3 乱序执行优化 三、java内存模型3.1 JVM对Java内存模型的实现3.2 Java内存模型和硬件架构之间的桥接3.3 Java内存模型 - 同步八种操作3.4 Java内存模…

jvm内存模型和java内存模型

初识java虚拟机,就碰到一系列不懂的问题。我们以前常说java把局部变量放在栈里,new出来的变量放在堆里,然后堆里的数据不定时就给回收了。然后,如果是多线程的话,每个线程自己都有会一个私有的虚拟机栈,运行…

JAVA内存模型与JVM内存模型的区别

** JAVA内存模型与JVM内存模型的区别 ** 直接进入正题 **JAVA内存模型: Java内存模型规定所有的变量都是存在主存中,每个线程都有自己的工作内存。线程堆变量的操作都必须在工作内存进行,不能直接堆主存进行操作,并且每个线程…

【3】Java内存模型

目录 知识点1:Java内存模型 知识点2:Volatile 1、什么是Volatile 2、Volatile特性 3、Volatile与Synchronized区别 知识点3:重排序 1、数据依赖性 2、as-if-serial语义 3、程序顺序规则 4、重排序对多线程的影响 知识点1&#xff1a…

Java内存模型详解

文章目录 一、什么是JMM?为什么需要JMM?二、JMM 如何抽象线程和主内存之间的关系?三、Java 内存区域和 JMM 有何区别?四、happens-before原则五、总结 一、什么是JMM?为什么需要JMM? Java 是最早尝试提供内…

java内存模型概述

java内存模型 为了控制线程之间的通信,(完成底层封装) 用来屏蔽掉各种硬件和操作系统之间的内存访问差异,以实现让Java程序在各平台下都能达到一致的内存访问效果。 JMM目标:定义程序中各个变量的访问规则&#xff0c…

理解Java内存模型(JMM)

本篇的写作思路是先阐明Java内存区域划分、硬件内存架构、Java多线程的实现原理与Java内存模型的具体关系,在弄明白它们间的关系后,进一步分析Java内存模型作用以及一些实现手段 理解Java内存区域与Java内存模型 Java内存区域 Java虚拟机在运行程序时会…

Java 内存模型

Java 内存模型(Java Memory Model),简称 JMM。 JVM 中试图定义一种 JMM 来屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。 一、物理内存模型 物理机遇到的并发问题与虚拟机中…

Java内存模型

Java内存模型 1.内存模型概念图2.线程共享区方法区常量池运行时常量池全局字符串池class文件常量池 堆空间3.线程独占区虚拟机栈本地方法栈程序计数器 4.对象的创建 1.内存模型概念图 2.线程共享区 方法区 方法区也是线程共享区用于储存虚拟机加载的类信息(类的版…

一文看懂Java内存模型(JMM)

文章目录 Java内存模型介绍总览图Java内存模型图线程、主内存、工作内存关系图CPU缓存架构图Java内存模型与硬件内存架构的关系 什么是Java内存模型Java内存模型的意义Java内存模型规范 Java内存模型的主要结构1、程序计数器(Program Counter Register)2…

SPSS建立时间序列模型

原始数据可能并不是时间序列,因此,对这样的数据建立时间序列模型,分为以下几步: 1 生成时间序列:定义新的时间变量,并对原始数据进行平稳处理 a)定义时间变量: 选择按什么时间序列进行定义&am…

时间序列模型(ARIMA和ARMA)完整步骤详述

我于2019年发布此篇文章至今收获了许多人的指点,当时的代码的确晦涩难懂,近期有空,将代码重新整理了一遍,重新发送至此。希望能够帮助大家更好地理解。 建模步骤: 目录 数据包和版本申明 步骤一:数据准备…

机器学习之时间序列模型

一、时间序列概念 在生产和科学研究中,对某一个或一组变量x(t)进行观察测量,将在一系列时刻t1, t2, …, tn (t为自变量)按照时间次序排列,并用于解释变量和相互关系的数学表达式。在相等的时间间隔内收集到的不同时间点的数据集合我们称之为时…

数学建模学习笔记(十)——时间序列模型

文章目录 一、时间序列综述二、时间序列数据以及基本概念三、时间序列分解四、指数平滑模型五、一元时间序列分析的模型六、AR(p)模型七、MA(q)模型八、ARMA(p, q)模型九、模型选择:AIC 和 BIC …

时间序列模型步骤教程(ARIMA)

目录 0、前言一、数据准备&探索1、平稳性1.1 平稳性检验1.2 数据处理(平滑、变换、差分、分解)1.2.1 对数变换1.2.2 平滑法(移动平均&指数平均)1.2.3 差分法1.2.4 分解 2、非白噪声检验 二、模型(ARIMA&#x…

时间序列模型分析

目录 一个引言 定义 确定性时间序列分析方法概述 确定性时间序列模型类型 移动平均法 简单移动平均法 加权移动平均法 趋势移动平均法 指数平滑法 一次指数平滑法 1.预测模型​编辑 2.加权系数的选择​编辑 3.初始值的确定​编…

时间序列模型相关说明和模型介绍

一、什么是时间序列? 时间序列是在规律性时间间隔记录的观测值序列。依赖于观测值的频率,典型的时间序列可分为每小时、每天、每周、每月、每季度和每年为单位记录。 import pandas as pd df pd.read_csv(https://raw.githubusercontent.com/selva86/datasets/d…

常用的时间序列模型

白噪声模型 时间序列算法之ARIMA模型 对非平稳时间序列的分析方法可以分为确定性因素分解的时序分析和随机时序分析两个大类。 确定性因素分解的方法把所有序列的变化都归结为4个因素:长期趋势、季节变动、循环变动和随机波动。其中长期趋势和季节变动的规律性信息通…