【面试突击】Java内存模型实战

时间:2024-01-25 12:25:35

欢迎关注公众号【11来了】,及时收到 AI 前沿项目工具及新技术的推送!

在我后台回复 「资料」 可领取编程高频电子书

在我后台回复「面试」可领取硬核面试笔记



前言

最近在更新面试突击专栏,我把每一篇将字数都尽量控制在 2000 字以内,可能在文章里边写的没有那么细致,主要是提供一些 问题 以及 回答的思路 ,以及 面试中可能忽略的漏洞 ,所以在看完文章之后,如果自己简历中有这方面的内容的话,一定要认真去整理一份自己的回答,并且多查阅相关资料,如果看的文章少,就会导致学习到的内容太片面

Java 内存模型

这块属于是 JVM 中的内容了,JVM 中的面试内容也是比较多的,包括常用的垃圾回收算法、垃圾回收器、堆、栈等等..

Java 内存模型(即 JMM)是在 《Java 虚拟机规范》 中定义的,目的是:定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节

JMM 规定了所有变量存储在主内存,每个线程都有自己的工作内存,线程 A 和线程 B 如果需要通信的话,需要经过 2 个步骤:

  1. 线程 A 把工作内存 A 中更新过的值刷新到主内存中
  2. 线程 B 去主内存中读取线程 A 刚更新过的值

【面试突击】Java内存模型实战_Java


Java 内存模型中的原子性、有序性、可见性是什么?

  • 原子性:一个操作以原子的方式执行,要么该操作不执行,要么执行过程中不可以被其他线程中断,就比如多线程环境下,i++ 必须是独立执行的,因为 i++ 不是原子操作,如果多线程同时执行,就会出现问题
  • 可见性:多个线程共享一个变量时,需要保证其中一个线程修改变量之后,被其他线程所感知到,并及时读取变量最新值
  • 有序性:指程序执行的顺序按照代码的先后顺序执行。在并发环境中,为了提高效率,编译器和处理器可能会对代码进行重排序,但是这种重排序不会影响单线程程序的执行,却可能影响到多线程并发执行的正确性

volatile 底层原理

如果面试中问到了 volatile 关键字,应该从 Java 内存模型开始讲解,再说到原子性、可见性、有序性是什么

之后说 volatile 解决了有序性可见性,但是并不解决原子性

volatile 可以说是 Java 虚拟机提供的最轻量级的同步机制,在很多开源框架中,都会大量使用 volatile 保证并发下的有序性和可见性

volatile 实现 可见性有序性 就是基于 内存屏障 的:

内存屏障是一种 CPU 指令,用于控制特定条件下的重排序和内存可见性问题

  • 写操作时,在写指令后边加上 store 屏障指令,让线程本地内存的变量能立即刷到主内存中
  • 读操作时,在读指令前边加上 load 屏障指令,可以及时读取到主内存中的值

JMM 中有 4 类内存屏障:(Load 操作是从主内存加载数据,Store 操作是将数据刷新到主内存)

  • LoadLoad:确保该内存屏障前的 Load 操作先于屏障后的所有 Load 操作。对于屏障前后的 Store 操作并无影响屏障类型
  • StoreStore:确保该内存屏障前的 Store 操作先于屏障后的所有 Store 操作。对于屏障前后的Load操作并无影响
  • LoadStore:确保屏障指令之前的所有Load操作,先于屏障之后所有 Store 操作
  • StoreLoad:确保屏障之前的所有内存访问操作(包括Store和Load)完成之后,才执行屏障之后的内存访问操作。全能型屏障,会屏蔽屏障前后所有指令的重排

在字节码层面上,变量添加 volatile 之后,读取和写入该变量都会加入内存屏障:

读取 volatile 变量时,在后边添加内存屏障,不允许之后的操作重排序到读操作之前

volatile变量读操作
LoadLoad 
LoadStore

写入 volatile 变量时,前后加入内存屏障,不允许写操作的前后操作重排序

LoadStore
StoreStore 
volatile变量写操作
StoreLoad



接下来了解一下 happens-before 是什么?

happens-before 原则是对 Java 内存模型的简化,帮助编程人员理解并发安全,happens-before 定义了一些规则,只要符合了这些规则,这些执行的先后关系就已经被确定了,不需要再通过 volatile 和 synchronized 来保证有序性,如果不符合这些规则,那么它们的执行就没有顺序性的保障,虚拟机可能会进行重排序!

  • 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
  • 管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。这里必须强调的是同一个锁,而 “后面” 是指时间上的先后顺序。
  • volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,这里的 “后面” 同样是指时间上的先后顺序。
  • 线程启动规则(Thread Start Rule):Thread 对象的 start () 方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread.join () 方法结束、Thread.isAlive () 的返回值等手段检测到线程已经终止执行。
  • 线程中断规则(Thread Interruption Rule):对线程 interrupt () 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted () 方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize () 方法的开始。
  • 传递性(Transitivity):如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。

对于 happens-before 原则,不需要记它具体有哪些规则,也根本记不住,只要了解 happens-before 包括了一些规则,符合这些规则的情况下,有序性就已经被保证了,那么就不需要通过 volatile 去保证有序性


为什么需要 happens-before 原则呢?

这里也是为了大家理解,说一下为什么会需要这个原则

如果在 Java 内存模型中,所有代码的有序性都依靠 volatile 和 synchronized 去保证,那么很多操作将会变得非常罗嗦

而我们在编写代码时,并没有使用很多的 volatile 和 synchronized 去保证有序性,就是因为 Java 语言中的 happens-before 原则的存在

通过这个原则,就可以很快判断并发环境中,两个操作之间是否会存在冲突的问题