理解Java内存模型,java类加载器面试题

文章探讨了乱序执行在多核处理器中的影响,以及Java内存模型如何通过内存一致性、指令重排序规则和内存屏障来确保跨线程操作的正确性。它还介绍了Happen-Before关系在内存模型中的作用,以及volatile和synchronized在维护可见性和有序性中的角色。
摘要由CSDN通过智能技术生成

代码执行乱序优化

乱序执行技术是处理器为提高运算速度而做出违背代码原有顺序的优化。在单核时代,处理器保证做出的优化不会导致执行结果远离预期目标,但在多核环境下却并非如此。

多核环境下, 如果存在一个核的计算任务依赖另一个核 计的算任务的中间结果,而且对相关数据读写没做任何防护措施,那么其顺序性并不能靠代码的先后顺序来保证,处理器最终得出的结果和我们逻辑得到的结果可能会大不相同。

代码乱序执行优化的问题

以上图为例进行说明:CPU的core2中的逻辑B依赖core1中的逻辑A先执行

  • 正常情况下,逻辑A执行完之后再执行逻辑B。

  • 在处理器乱序执行优化情况下,有可能导致flag提前被设置为true,导致逻辑B先于逻辑A执行。

2 Java内存模型的组成分析

===============

内存模型概念


为了更好解决上面提到系列问题,内存模型被总结提出,我们可以把内存模型理解为在特定操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象

不同架构的物理计算机可以有不一样的内存模型,Java虚拟机也有自己的内存模型。Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果,不必因为不同平台上的物理机的内存模型的差异,对各平台定制化开发程序。

更具体一点说,Java内存模型提出目标在于,定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数值对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的。(如果局部变量是一个reference类型,它引用的对象在Java堆中可被各个线程共享,但是reference本身在Java栈的局部变量表中,它是线程私有的)。

如果你想要学习Java的话,我给你分享一些Java的学习资料,你不用浪费时间到处搜了,从Java入门到精通的资料我都给你整理好了,这些资料都是我做Java这几年整理的Java最新学习路线,Java笔试题,Java面试题,Java零基础到精通视频课程,Java开发工具,Java练手项目,Java电子书,Java学习笔记,PDF文档教程,Java程序员面经,Java求职简历模板等,这些资料对你接下来学习Java一定会带来非常大的帮助,每个Java初学者都必备,请你进我的**Java技术qq交流群**自行下载,所有资料都在群文件里,进去要跟大家多交流学习哦。

Java内存模型的组成


  • 主内存 Java内存模型规定了所有变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件的主内存名字一样,两者可以互相类比,但此处仅是虚拟机内存的一部分)。

  • 工作内存 每条线程都有自己的工作内存(Working Memory,又称本地内存,可与前面介绍的处理器高速缓存类比),线程的工作内存中保存了该线程使用到的变量的主内存中的共享变量的副本拷贝。工作内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

Java内存模型抽象示意图如下:

Java内存模型抽象示意图

JVM内存操作的并发问题


结合前面介绍的物理机的处理器处理内存的问题,可以类比总结出JVM内存操作的问题,下面介绍的Java内存模型的执行处理将围绕解决这2个问题展开:

  • 1 工作内存数据一致性 各个线程操作数据时会保存使用到的主内存中的共享变量副本,当多个线程的运算任务都涉及同一个共享变量时,将导致各自的的共享变量副本不一致,如果真的发生这种情况,数据同步回主内存以谁的副本数据为准? Java内存模型主要通过一系列的数据同步协议、规则来保证数据的一致性,后面再详细介绍。

  • 2 指令重排序优化 Java中重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:编译期重排序和运行期重排序,分别对应编译时和运行时环境。 同样的,指令重排序不是随意重排序,它需要满足以下两个条件:

  • 1 在单线程环境下不能改变程序运行的结果 即时编译器(和处理器)需要保证程序能够遵守 as-if-serial 属性。通俗地说,就是在单线程情况下,要给程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果保持一致。

  • 2 存在数据依赖关系的不允许重排序

多线程环境下,如果线程处理逻辑之间存在依赖关系,有可能因为指令重排序导致运行结果与预期不同,后面再展开Java内存模型如何解决这种情况。

3 Java内存间的交互操作

==============

在理解Java内存模型的系列协议、特殊规则之前,我们先理解Java中内存间的交互操作。

交互操作流程


为了更好理解内存的交互操作,以线程通信为例,我们看看具体如何进行线程间值的同步:

线程间交互操作

线程1和线程2都有主内存中共享变量x的副本,初始时,这3个内存中x的值都为0。线程1中更新x的值为1之后同步到线程2主要涉及2个步骤:

  • 1 线程1把线程工作内存中更新过的x的值刷新到主内存中

  • 2 线程2到主内存中读取线程1之前已更新过的x变量

从整体上看,这2个步骤是线程1在向线程2发消息,这个通信过程必须经过主内存。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,实现各个线程提供共享变量的可见性。

内存交互的基本操作


关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了下面介绍8种操作来完成。

虚拟机实现时必须保证下面介绍的每种操作都是原子的,不可再分的(对于double和long型的变量来说,load、store、read、和write操作在某些平台上允许有例外,后面会介绍)。

8种基本操作

8种基本操作

  • lock (锁定) 作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

  • unlock (解锁) 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

  • read (读取) 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

  • load (载入) 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

  • use (使用) 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时就会执行这个操作。

  • assign (赋值) 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  • store (存储) 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后write操作使用。

  • write (写入) 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

如果你想要学习Java的话,我给你分享一些Java的学习资料,你不用浪费时间到处搜了,从Java入门到精通的资料我都给你整理好了,这些资料都是我做Java这几年整理的Java最新学习路线,Java笔试题,Java面试题,Java零基础到精通视频课程,Java开发工具,Java练手项目,Java电子书,Java学习笔记,PDF文档教程,Java程序员面经,Java求职简历模板等,这些资料对你接下来学习Java一定会带来非常大的帮助,每个Java初学者都必备,请你进我的**Java技术qq交流群**自行下载,所有资料都在群文件里,进去要跟大家多交流学习哦。

4 Java内存模型运行规则

==============

4.1 内存交互基本操作的3个特性


在介绍内存的交互的具体的8种基本操作之前,有必要先介绍一下操作的3个特性,Java内存模型是围绕着在并发过程中如何处理这3个特性来建立的,这里先给出定义和基本实现的简单介绍,后面会逐步展开分析。

  • 原子性(Atomicity) 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

  • 可见性(Visibility) 是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 正如上面“交互操作流程”中所说明的一样,JMM是通过在线程1变量工作内存修改后将新值同步回主内存,线程2在变量读取前从主内存刷新变量值,这种依赖主内存作为传递媒介的方式来实现可见性。

  • 有序性(Ordering) 有序性规则表现在以下两种场景: 线程内和线程间

  • 线程内 从某个线程的角度看方法的执行,指令会按照一种叫“串行”(as-if-serial)的方式执行,此种方式已经应用于顺序编程语言。

  • 线程间 这个线程“观察”到其他线程并发地执行非同步的代码时,由于指令重排序优化,任何代码都有可能交叉执行。唯一起作用的约束是:对于同步方法,同步块(synchronized关键字修饰)以及volatile字段的操作仍维持相对有序。

Java内存模型的一系列运行规则看起来有点繁琐,但总结起来,是围绕原子性、可见性、有序性特征建立。归根究底,是为实现共享变量的在多个线程的工作内存的数据一致性,多线程并发,指令重排序优化的环境中程序能如预期运行。

4.2 happens-before关系


介绍系列规则之前,首先了解一下happens-before关系:用于描述下2个操作的内存可见性:如果操作A happens-before 操作B,那么A的结果对B可见。happens-before关系的分析需要分为单线程和多线程的情况:

  • 单线程下的 happens-before 字节码的先后顺序天然包含happens-before关系:因为单线程内共享一份工作内存,不存在数据一致性的问题。 在程序控制流路径中靠前的字节码 happens-before 靠后的字节码,即靠前的字节码执行完之后操作结果对靠后的字节码可见。然而,这并不意味着前者一定在后者之前执行。实际上,如果后者不依赖前者的运行结果,那么它们可能会被重排序。

  • 多线程下的 happens-before 多线程由于每个线程有共享变量的副本,如果没有对共享变量做同步处理,线程1更新执行操作A共享变量的值之后,线程2开始执行操作B,此时操作A产生的结果对操作B不一定可见。

为了方便程序开发,Java内存模型实现了下述支持happens-before关系的操作:

  • 程序次序规则 一个线程内,按照代码顺序,书写在前面的操作 happens-before 书写在后面的操作。

  • 锁定规则 一个unLock操作 happens-before 后面对同一个锁的lock操作。

  • volatile变量规则 对一个变量的写操作 happens-before 后面对这个变量的读操作。

  • 传递规则 如果操作A happens-before 操作B,而操作B又 happens-before 操作C,则可以得出操作A happens-before 操作C。

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

  • 线程中断规则 对线程interrupt()方法的调用 happens-before 被中断线程的代码检测到中断事件的发生。

  • 线程终结规则 线程中所有的操作都 happens-before 线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。

  • 对象终结规则 一个对象的初始化完成 happens-before 他的finalize()方法的开始

4.3 内存屏障


Java中如何保证底层操作的有序性和可见性?可以通过内存屏障。

内存屏障是被插入两个CPU指令之间的一种指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障有序性的。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障可见性

举个例子:

Store1;

Store2;

Load1;

StoreLoad; //内存屏障

Store3;

Load2;

Load3;

复制代码

对于上面的一组CPU指令(Store表示写入指令,Load表示读取指令),StoreLoad屏障之前的Store指令无法与StoreLoad屏障之后的Load指令进行交换位置,即重排序。但是StoreLoad屏障之前和之后的指令是可以互换位置的,即Store1可以和Store2互换,Load2可以和Load3互换。

常见有4种屏障

  • LoadLoad屏障: 对于这样的语句 Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

  • StoreStore屏障: 对于这样的语句 Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

  • LoadStore屏障: 对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被执行前,保证Load1要读取的数据被读取完毕。

  • StoreLoad屏障: 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的(冲刷写缓冲器,清空无效化队列)。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

Java中对内存屏障的使用在一般的代码中不太容易见到,常见的有volatile和synchronized关键字修饰的代码块(后面再展开介绍),还可以通过Unsafe这个类来使用内存屏障。

4.4 8种操作同步的规则


JMM在执行前面介绍8种基本操作时,为了保证内存间数据一致性,JMM中规定需要满足以下规则:

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
img

最后

分享一些系统的面试题,大家可以拿去刷一刷,准备面试涨薪。

这些面试题相对应的技术点:

  • JVM
  • MySQL
  • Mybatis
  • MongoDB
  • Redis
  • Spring
  • Spring boot
  • Spring cloud
  • Kafka
  • RabbitMQ
  • Nginx

大类就是:

  • Java基础
  • 数据结构与算法
  • 并发编程
  • 数据库
  • 设计模式
  • 微服务
  • 消息中间件

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

程序员,每个月给你发多少工资,你才会想老板想的事?

息中间件

[外链图片转存中…(img-ga5eX0uL-1710761240187)]

[外链图片转存中…(img-axIYVpir-1710761240187)]

[外链图片转存中…(img-I1NJV9Uq-1710761240188)]

[外链图片转存中…(img-89cHF4QX-1710761240188)]

[外链图片转存中…(img-pt8V35BH-1710761240188)]

[外链图片转存中…(img-QPSi5nx7-1710761240188)]

[外链图片转存中…(img-hWPjTPno-1710761240189)]

[外链图片转存中…(img-ZvmYFREa-1710761240189)]

[外链图片转存中…(img-xdOIA9pa-1710761240189)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值