Java内存模型(附牛客大佬整理19年最全面经)

写在前面: 前段时间在牛客上看面经看到了一位学长的面经,记录了他整理了面试知识点,也算是我逛牛客这么久以来看到的最全的知识点整理。正好这段时间也在收集题目进行整理与消化。所以就打算以这个为模板先进进整理与复习,这里先贴出一部分,后序的再行整理。
持续更新面试高频题目与知识点,正在打算面试的小伙伴可以持续关注。
牛客地址

前言: 本篇文章有借鉴一些其他的博文,但是总是感觉其他的博文在初读起来的时候总是一头雾水,觉得只是简单的拼凑起来,不能够深入了解之间的关系,这里也是在借鉴博文的同时也阅读了《深入理解Java虚拟机》来写出这篇文章 ,希望能为大家答疑解惑。建议大家在学习这方面的内容时候参看《深入理解Java虚拟机》,比较全面有循序渐入的认知。

Java内存模型

在学习一个知识的时候,第一点首先要明确其定义:是做什么的
Java内存模型被称为是 (JMM)这里要与java的虚拟机(JVM)进行区分
JVM:是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的
JMM: 是一种抽象的概念,并不是真实存在的,描述了一组规则或规范,通过这个规范定义了程序在各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式,试图屏蔽各种硬件和操作系统的内存访问差异。 (用在哪里:可以在多线程并发时候,解决多线程之前的通信问题和解决线程操作的原子性,可见性,有序性。)

主内存与工作内存

至于这里为什么又突然讲到了主内存工作内存呢? 前面也讲到了 Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存取出变量这样的底层细节
首先看一张图片:
在这里插入图片描述
在处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这样的问题,在他们之间加入了高速缓存。但是加入高速缓存以后,又出现了一个问题,缓存的一致性。如果多个缓存共享一块主内存区域,那么多个缓存缓存的数据就可能会不一致。这个时候就使用到了缓存一致性协议(具体这个协议如何实现我也不得而至)。 对于所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存器中,同时对于线程只能操作工作内存中的变量,但是对于不同的线程在互相传递值的时候,需要通过主存来完成。

数据存储类型以及操作方式

  • 方法中的基本类型本地变量将直接存储在工作内存的栈帧结构中。
  • 引用类的本地变量: 引用存储在工作内存中,世界存储在主存中。
  • 成员变量,静态变量,类型均会被存储在主内存中;
  • 主内存共享的方法是线程个拷贝一份数据到工作内存中,操作完成后就刷新到主内存中。

内存间交互操作

在讲述完主内存与内存之间的关系以后,我们开始思考主内存与工作内存之间的具体的协议是怎样的呢,即一个变量又是如何从主内存拷贝到工作内存中。这些细节是如何实现的呢,于是我们来介绍以下的8个操作(注:虚拟机实现时候必须保证下面体积的每一种操作都是原子的不可再分的
讲到内存间的相互操作时候,就要讲到主内存与 工作内存之间的关系
Java内存模型定义了8个操作还完成主内存和工作内存
在这里插入图片描述

  • read: 把一个变量的值 从主内存传输到工作内存中。
  • load: 在read执行以后,把read得到的值放入到工作内存的变量副本中。
  • use: 把工作内存中一个变量的值传递给执行引擎。
  • assign: 把一个从执行引擎接收到的值传递给工作内存。
  • store: 把工作内存中的一个变量传送到主存中。
  • write: 在store执行以后,把store得到的值 放入到主内存的变量中。
  • lock: 用作于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock 用作于主内存的变量,它把一个处于锁定状态的变量释放出来,释放以后的变量才能够被其他的线程锁定。

对于 volatile 型变量的特殊规则

对于 关键字 volat来说 是Java虚拟机提供的轻量级同步机制,之前在学习Java并发的时候胡也会对这个关键字有设计,这里我们的使用有两个特性:

保证变量对所有线程可见

这个我们不再过多的介绍说的是当一条线程修改了一个变量的值,新值对于其他的线程来说都是可见的,但是对于普通的变量来说 在修改以后,均需要通过主内存来进行一次的写入与读取以后才能够让其他的线程对这个值可见。

能够禁止指令重新排序优化

什么指令重排序:重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。
指令重排的条件

  • 在 单线程环境下不能改变程序的运行结果;
  • 存在数据依赖关系的不允许重排序;
  • 无法通过Happens-before原则推到出来的,才能进行指令的重排序。

内存模型的三大特性

在介绍完Java内存模型的相关操作和规则时候,我们来整体回顾这个模型的特征。Java内存模型是围绕着在并发的过程中如何处理原子性可见性有序性而建立的,下面我们来具体介绍。

原子性

即一个操作或者多个操作要么是全部执行,并且在执行的过程中不能够被打断,要么全部都执行完成,要么都不执行。

可见性

多个线程在访问同一个变量的时候,一个线程修改了这个变量的值,其他的线程也就能够立即看到这个修改的值。(正如我们在上面讲到的
JMM 是通过在线程 1 变量工作内存修改后将新值同步回主内存,线程 2 在变量读取前从主内存刷新变量值,这种依赖主内存作为传递媒介的方式来实现可见性)

有序性

有序性是指在本线程内观察,所有的操作都是有序的,在一个线程观察另外一个线程的时候,所以的操作都是无序的,无序是因为发生了指令重排序。在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会印象多线程并发执行的正确性。

同时对于 Volatile 关键字来说 通过添加内存屏障的方式来禁止指令的重排序。即重排序的时候不能够把后面的指令放到内存屏障的前面。

也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。表示了持有同一个锁的同步块只能串行进入。

先行发生原则

通过前面的介绍我们发现的是,对于Java内存模型中的有序性基本上是通过volatilesynchronized来完成的,此时有些的操作就会很麻烦,但是我们察觉不到的原因就是因为在java中有一个先行发生的原则。通过来可以判断数据直接是否存在竞争,线程是否安全的主要依据,我们依靠这个原则,可以通过几条规则一揽子地解决并发环境下两个操作之间是否存在冲突的所有问题

了解了先行发生原则的来源是什么。。下面我们来看一看其到底指的是什么: 指的是Java内存模型中定义的两项操作之间的偏序关系。如果一个A操作先行发生在另一个操作B的前面,其实就是说在发生操作B之前,操作A产生的影响会被操作B观察到。这个影响包括“修改了内存中共享变量的值,发送了消息,调用了方法等。”
下面是一些在Java内存模型下一些“天然的”先行发生关系,这些关系无序任何的同步器协助时候就已经出现了,可以直接使用。若是两个操作之前的关系不在下面的这些情况下,并且无法推倒出来时候,他们就没有任何的顺序性保障,此时的虚拟机也就可以对他们进行随意的重排序。

  1. 程序次序规则: 在一个线程内时候,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确的来说应该是控制流顺序,还不是程序代码顺序,因为要考虑到分支,循环等结构。
  2. 管城锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
  3. volatile规则: 对于一个volatile变量的写操作先行与后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
  4. 线程启动规则: Thread对象的start()方法先行发生于此线程的每一个动作。
  5. 线程终止规则: 线程中的所有操作都先行发生于对此线程的终止检测,可以通过 Thread.join()方法结束,Thread.isAlive() 的返回值等手段检测到线程已经终止执行。
  6. 线程中断规则: 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生,可以通过 Thread.interrupted() 方法检测到是否有中断发生。
  7. 对象终结规则: 一个对象的初始化完成(构找函数执行结束)先发生于她的 finalize() 方法的开始。
  8. 传递性: 如果操作 A先行发生于 操作B,操作B先行发生于 操作C ,此时可以说 A先发生于 C。

后记

以上就是个人认为还算是比较重要的知识点,一般面试官会直接问题说一首Java内存模型 就可以说其是干嘛的(概念),可以用在什么地方,解决了什么问题,然后再表述其三大特性,就设计到了指令的重排,以及关键字 volatile,既然说到了 这个关键字,你就必须要了解 synchronized关键字等等知识点。就可以将其全部串起来。也就对面试不在惧怕。

发布了26 篇原创文章 · 获赞 5 · 访问量 714

猜你喜欢

转载自blog.csdn.net/weixin_44015043/article/details/104595728