Java并发编程面试题:内存模型(6题)

news2025/1/11 5:22:46

🧑 博主简介:CSDN博客专家历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编程高并发设计Springboot和微服务,熟悉LinuxESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
技术合作请加本人wx(注明来自csdn):foreast_sea

在这里插入图片描述


在这里插入图片描述

Java并发编程面试题:内存模型(6题)

1. 说一下你对 Java 内存模型的理解?

Java 内存模型Java Memory Model)是一种抽象的模型,简称 JMM,主要用来定义多线程中变量的访问规则,用来解决变量的可见性、有序性和原子性问题,确保在并发环境中安全地访问共享变量。

深入浅出 Java 多线程:Java内存模型

JMM 定义了线程内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了共享变量的副本,用来进行线程内部的读写操作。

  • 当一个线程更改了本地内存中共享变量的副本后,它需要将这些更改刷新到主内存中,以确保其他线程可以看到这些更改。
  • 当一个线程需要读取共享变量时,它可能首先从本地内存中读取。如果本地内存中的副本是过时的,线程将从主内存中重新加载共享变量的最新值到本地内存中。

本地内存是 JMM 中的一个抽象概念,并不真实存在。实际上,本地内存可能对应于 CPU 缓存、寄存器或者其他硬件和编译器优化。

在这里插入图片描述

对于一个双核 CPU 的系统架构,每个核都有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。

每个核都有自己的一级缓存,在有些架构里面还有一个所有 CPU 共享的二级缓存。

Java 内存模型里面的本地内存,可能对应的是 L1 缓存或者 L2 缓存或者 CPU 寄存器。

为什么线程要用自己的内存?

第一,在多线程环境中,如果所有线程都直接操作主内存中的共享变量,会引发更多的内存访问竞争,这不仅影响性能,还增加了线程安全问题的复杂度。通过让每个线程使用本地内存,可以减少对主内存的直接访问和竞争,从而提高程序的并发性能。

第二,现代 CPU 为了优化执行效率,可能会对指令进行乱序执行(指令重排序)。使用本地内存(CPU 缓存和寄存器)可以在不影响最终执行结果的前提下,使得 CPU 有更大的自由度来乱序执行指令,从而提高执行效率。

2. 说说你对原子性、可见性、有序性的理解?

  • 原子性:指的是一个操作是不可分割的,要么全部执行成功,要么完全不执行。
  • 可见性:指的是一个线程对共享变量的修改,能够被其他线程及时看见。
  • 有序性:指的是程序代码的执行顺序与代码中的顺序一致。在没有同步机制的情况下,编译器可能会对指令进行重排序,以优化性能。这种重排序可能会导致多线程的执行结果与预期不符。

分析下面几行代码的原子性?

int i = 2;
int j = i;
i++;
i = i + 1;
  • 第 1 句是基本类型赋值,是原子性操作。
  • 第 2 句先读 i 的值,再赋值到 j,两步操作,不能保证原子性。
  • 第 3 和第 4 句其实是等效的,先读取 i 的值,再+1,最后赋值到 i,三步操作了,不能保证原子性。

原子性、可见性、有序性都应该怎么保证呢?

  • 原子性:JMM 只能保证基本的原子性,如果要保证一个代码块的原子性,需要使用synchronized
  • 可见性:Java 是利用volatile关键字来保证可见性的,除此之外,finalsynchronized也能保证可见性。
  • 有序性:synchronized或者volatile都可以保证多线程之间操作的有序性。

i++是原子操作吗?

i++ 不是一个原子操作,它包括三个步骤:

  1. 从内存中读取 i 的值。
  2. 对 i 进行加 1 操作。
  3. 将新的值写入内存。

假如两个线程同时对 i 进行 i++ 操作时,可能会发生以下情况:

  1. 线程 A 读取 i 的值(假设 i 的初始值为 1)。
  2. 线程 B 也读取 i 的值(值仍然是 1)。
  3. 线程 A 将 i 增加到 2,并将其写回内存。
  4. 线程 B 也将 i 增加到 2,并将其写回内存。

尽管进行了两次递增操作,i 的值只增加了 1 而不是 2。可以使用 synchronized 或 AtomicInteger 确保操作的原子性。

3. 那说说什么是指令重排?

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

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

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

多级指令重排

我们比较熟悉的双重校验单例模式就是一个经典的指令重排的例子,Singleton instance=new Singleton();对应的 JVM 指令分为三步:分配内存空间–>初始化对象—>对象指向分配的内存空间,但是经过了编译器的指令重排序,第二步和第三步就可能会重排序。

在这里插入图片描述

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

4. 指令重排有限制吗?happens-before 了解吗?

指令重排也是有一些限制的,有两个规则happens-beforeas-if-serial来约束。

happens-before 的定义

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

happens-before 和我们息息相关的有六大规则:

在这里插入图片描述

  • 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
  • volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
  • 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
  • start()规则:如果线程 A 执行操作 ThreadB.start()(启动线程 B),那么 A 线程的 ThreadB.start()操作 happens-before 于线程 B 中的任意操作。
  • join()规则:如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功返回。

5. as-if-serial 又是什么?单线程的程序一定是顺序的吗?

as-if-serial 语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。编译器、runtime 和处理器都必须遵守 as-if-serial 语义。

为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。为了具体说明,请看下面计算圆面积的代码示例。

double pi = 3.14;   // A
double r = 1.0;   // B
double area = pi * r * r;   // C

上面 3 个操作的数据依赖关系:

A 和 C 之间存在数据依赖关系,同时 B 和 C 之间也存在数据依赖关系。因此在最终执行的指令序列中,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面,程序的结果将会被改变)。但 A 和 B 之间没有数据依赖关系,编译器和处理器可以重排序 A 和 B 之间的执行顺序。

所以最终,程序可能会有两种执行顺序:

两种执行结果

as-if-serial 语义把单线程程序保护了起来,遵守 as-if-serial 语义的编译器、runtime 和处理器共同编织了这么一个“楚门的世界”:单线程程序是按程序的“顺序”来执行的。as- if-serial 语义使单线程情况下,我们不需要担心重排序的问题,可见性的问题。

6. volatile 了解吗?

volatile 关键字主要有两个作用,一个是保证变量的内存可见性,一个是禁止指令重排序。它确保一个线程对变量的修改对其他线程立即可见,同时防止代码执行顺序被编译器或 CPU 优化重排。

volatile 怎么保证可见性的呢?

当一个变量被声明为 volatile 时,Java 内存模型会确保所有线程看到该变量时的值是一致的。

深入浅出 Java 多线程:Java内存模型

当线程对 volatile 变量进行写操作时,JMM 会在写入这个变量之后插入一个写屏障指令,这个指令会强制将本地内存中的变量值刷新到主内存中。

在这里插入图片描述

在 x86 架构下,volatile 写操作会插入一个 lock 前缀指令,这个指令会将缓存行的数据写回到主内存中,确保内存可见性。

mov [a], 2          ; 将值 2 写入内存地址 a
lock add [a], 0     ; lock 指令充当写屏障,确保内存可见性

当线程对 volatile 变量进行读操作时,JMM 会插入一个 读屏障指令,这个指令会强制让本地内存中的变量值失效,从而重新从主内存中读取最新的值。

在这里插入图片描述

例如,我们声明一个 volatile 变量 x:

volatile int x = 0

线程 A 对 x 写入后会将其最新的值刷新到主内存中,线程 B 读取 x 时由于本地内存中的 x 失效了,就会从主内存中读取最新的值,内存可见性达成!

在这里插入图片描述

volatile 怎么保证有序性的呢?

在程序执行期间,为了提高性能,编译器和处理器会对指令进行重排序。但涉及到 volatile 变量时,它们必须遵循一定的规则:

  • 写 volatile 变量的操作之前的操作不会被编译器重排序到写操作之后。
  • 读 volatile 变量的操作之后的操作不会被编译器重排序到读操作之前。

这意味着 volatile 变量的写操作总是发生在任何后续读操作之前。

volatile 和 synchronized 的区别

volatile 关键字用于修饰变量,确保该变量的更新操作对所有线程是可见的,即一旦某个线程修改了 volatile 变量,其他线程会立即看到最新的值。

synchronized 关键字用于修饰方法或代码块,确保同一时刻只有一个线程能够执行该方法或代码块,从而实现互斥访问。

volatile 加在基本类型和对象上的区别?

volatile 用于基本数据类型时,能确保该变量的读写操作是直接从主内存中读取或写入的。

private volatile int count = 0;

volatile 用于引用类型时,它确保引用本身的可见性,即确保引用指向的对象地址是最新的。

但是,volatile 并不能保证引用对象内部状态的线程安全性。

private volatile SomeObject obj = new SomeObject();

虽然 volatile 确保了 obj 引用的可见性,但对 obj 引用的具体对象的操作并不受 volatile 保护。如果需要保证引用对象内部状态的线程安全,需要使用其他同步机制(如 synchronizedReentrantLock)。

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

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

相关文章

标准应用 | 2025年网络安全服务成本度量实施参考

01 网络安全服务成本度量依据相关新变化 为了解决我国网络安全服务产业发展中面临的服务供需两方对于服务成本组成认知偏差较大、网络安全服务成本度量缺乏依据的问题,中国网络安全产业联盟(CCIA)组织北京赛西科技发展有限责任公司、北京安…

太速科技-FMC141-四路 250Msps 16bits AD FMC子卡

FMC141-四路 250Msps 16bits AD FMC子卡 一、产品概述: 本板卡基于 FMC 标准板卡,实现 4 路 16-bit/250Msps ADC 功能。遵循 VITA 57 标准,板卡可以直接与xilinx公司或者本公司 FPGA 载板连接使用。板卡 ADC 器件采用 ADI 公司 AD9467 芯…

通义灵码在跨领域应用拓展之物联网篇

目录 一.引言 二.通义灵码简介 三.通义灵码在物联网领域的设备端应用 1.传感器数据采集 (1).不同类型传感器的数据读取 (2).数据转换与预处理 2.设备控制指令接收和执行 (1).指令解析与处理 (2).设备动作执行 四.通义灵码在物联网领域的云端平台应用 1.数据存储和管…

使用Kubernetes部署Spring Boot项目

目录 前提条件 新建Spring Boot项目并编写一个接口 新建Maven工程 导入 Spring Boot 相关的依赖 启动项目 编写Controller 测试接口 构建镜像 打jar包 新建Dockerfile文件 Linux目录准备 上传Dockerfile和target目录到Linux 制作镜像 查看镜像 测试镜像 上传镜…

C#基础之 继承类相关构造函数使用

类构造函数 作用是为 类中成员变量进行赋值操作 单个类的时候 一般不会有什么思路问题,主要说明一下 有继承关系类的时候 当存在继承关系的类 如 A:B 首先要注意第一点:顺序 那么在构造函数时 顺序是由 B先构造 然后 A在构造 注意第二点方法…

【leetcode刷题】:双指针篇(有效三角形的个数、和为s的两个数)

文章目录 一、有效三角形的个数题目解析算法原理代码编写 二、和为s的两个数题目解析算法原理代码编写 一、有效三角形的个数 题目解析 有效三角形的个数【点击跳转】 题目意思很好理解:就是在一堆非负整数的数组里,随机选三个数进行搭配,…

【Unity3D】apk加密(global-metadata.dat加密)

涉及:apk、aab、global-metadata.dat、jks密钥文件、APKTool、zipalign 使用7z打开apk文件观察发现有如下3个针对加密的文件。 xxx.apk\assets\bin\Data\Managed\Metadata\global-metadata.dat xxx.apk\lib\armeabi-v7a\libil2cpp.so xxx.apk\lib\arm64-v8a\libil…

机器学习之贝叶斯分类器和混淆矩阵可视化

贝叶斯分类器 目录 贝叶斯分类器1 贝叶斯分类器1.1 概念1.2算法理解1.3 算法导入1.4 函数 2 混淆矩阵可视化2.1 概念2.2 理解2.3 函数导入2.4 函数及参数2.5 绘制函数 3 实际预测3.1 数据及理解3.2 代码测试 1 贝叶斯分类器 1.1 概念 贝叶斯分类器是基于贝叶斯定理构建的分类…

前端报告 2024:全新数据,深度解析未来趋势

温馨提示: 此报告为国际版全球报告,其中所涉及的技术应用、工具偏好、开发者习惯等情况反映的是全球前端开发领域的综合态势。由于国内外技术发展环境、行业生态以及企业需求等存在差异,可能有些内容并不完全契合国内的实际情况,请大家理性阅读,批判性地吸收其中的观点与信…

科普CMOS传感器的工作原理及特点

在当今数字化成像的时代,图像传感器无疑是幕后的关键 “功臣”,它宛如一位神奇的 “光影魔法师”,通过光电效应这一奇妙的物理现象,将光子巧妙地转换成电荷,为图像的诞生奠定基础。而在众多类型的图像传感器中&#xf…

word论文排版常见问题汇总

word论文排版常见问题汇总 常用快捷键: Alt F9 正常模式与域代码模式切换 Ctrl F9 插入域代码 F9 刷新域代码显示,要注意选定后刷新才会有效果 word中在当前列表的基础上修改列表 在使用word时,我们会定义一个列表,并将其链接…

使用PVE快速创建虚拟机集群并搭建docker环境

安装Linux系统 这里以安装龙蜥操作系统AnolisOS8.9为例加以说明。 通过PVE后台上传操作系统ISO镜像。 然后在PVE上【创建虚拟机】,选定上传的龙蜥操作系统镜像进行系统安装。 注意:在安装过程中,要设定语言、时区、超管用户root的密码、普…

某音响制造公司发展战略转型项目成功案例纪实

面对产业结构变化、海外订单缩减、劳动力成本攀升、缺乏自主品牌等原因导致的利润空间急剧下降的挑战,面向海外市场的代工厂如何在严峻的经济形势下克服发展障碍,成功实现转型与发展? 某音响制造公司,面临着订单量减少、成本增高…

redis的学习(三)

6. set集合 集合:把一些有关联的数据放在一起。 1、集合中的元素是无序的,即数据存放顺序不重要,变化一下顺序,集合依旧是之前的集合。 2、集合中的元素是不能重复的(唯一性)与list类似的是集合中的每一个元…

点赞系统设计(微服务)

点赞业务是一个常见的社交功能,它允许用户对其他用户的内容(如帖子、评论、图片等)表示喜欢或支持。在设计点赞业务时,需要考虑以下几个方面: 一、业务需求 点赞业务需要满足以下特性: 通用:…

C#进阶-在Ubuntu上部署ASP.NET Core Web API应用

随着云计算和容器化技术的普及,Linux 服务器已成为部署 Web 应用程序的主流平台之一。ASP.NET Core 作为一个跨平台、高性能的框架,非常适合在 Linux 环境中运行。本篇博客将详细介绍如何在 Linux 服务器上部署 ASP.NET Core Web API 应用,包…

设计模式-结构型-桥接模式

1. 什么是桥接模式? 桥接模式(Bridge Pattern) 是一种结构型设计模式,它旨在将抽象部分与实现部分分离,使它们可以独立变化。通过这种方式,系统可以在抽象和实现两方面进行扩展,而无需相互影响…

python学习笔记—16—数据容器之元组

1. 元组——tuple(元组是一个只读的list) (1) 元组的定义注意:定义单个元素的元组,在元素后面要加上 , (2) 元组也支持嵌套 (3) 下标索引取出元素 (4) 元组的相关操作 1. index——查看元组中某个元素在元组中的位置从左到右第一次出现的位置 t1 (&qu…

基础算法--查找

一、线性枚举 1、线性枚举定义 线性枚举指的就是遍历某个一维数组(顺序表)的所有元素,找到满足条件的那个元素并且返回,返回值可以是下标,也可以是元素本身。 由于是遍历的,穷举了所有情况,所…

G1垃圾回收器的FullGC

如何确定GarbageFirst回收器发生的是FullGC ? 必须出现FullGC字样才算是FUllGC,例如下图:因为内存分配失败(Allocation Failure)导致 如果不出现FullGC的字样说明它不是FUllGC,并不像Serial GC、ParallelGC的在老年代…