共计 6113 个字符,预计需要花费 16 分钟才能阅读完成。
我去年以面试官的身份面了多个候选人,深知很多人其实并没有搞清楚 Java 的内存模型的概念和存在作用,当我在问谈谈 Java 的内存模型的时候,大多数人都答复了什么 JVM 的内存构造啊,也就是堆那些啊什么的,这些都是错的,那么实际上 Java 的内存模型实际上是什么呢?它的常问面试题又是什么呢?别急,我这边曾经给你整顿好了。
理解几个重要的概念。
CPU 和缓存一致性
咱们都晓得,计算机在执行程序的时候,每条指令都是在 CPU 中执行的,而执行的时候,又免不了和数据打交道,而计算机下面的数据,是寄存在计算机的物理内存上的。
当内存的读取速度和 CPU 的执行速度相比差异不大的时候,这样的机制是没有任何问题的,可是随着 CPU 的技术的倒退,CPU 的执行速度和内存的读取速度差距越来越大,导致 CPU 每次操作内存都要消耗很多等待时间。
为了解决这个问题,初代程序员大佬们想到了一个的方法,就是在 CPU 和物理内存上新增高速缓存,这样程序的执行过程也就产生了扭转,变成了程序在运行过程中,会将运算所须要的数据从主内存复制一份到 CPU 的高速缓存中,当 CPU 进行计算时就能够间接从高速缓存中读数据和写数据了,当运算完结再将数据刷新到主内存就能够了。
随着时代的变迁,程序员的越发无能,CPU 开始呈现了多核的概念,每个核都有一套本人的缓存,并且随着计算机能力一直晋升,还开始反对多线程,最终演变成,多个线程拜访过程中的某个共享内存,且这多个线程别离在不同的外围上执行,则每个外围都会在各自的 Cache 中保留一份共享内存的缓冲,咱们晓得多核是能够并行的,这样就会呈现多个线程同时写各自的缓存的状况,导致各自的 Cache 之间的数据可能不同。
总结下来就是:在多核 CPU 中,每个核的本人的缓存,对于同一个数据的缓存内容可能不统一。
处理器优化和指令重排
为了使处理器外部的运算单元可能被充分利用,处理器可能会对程序代码进行乱序执行解决,这就是处理器优化。
除了当初很多风行的处理器会对代码进行优化乱序解决,很多编程语言的编译器也会有相似的优化,比方 Java 虚拟机的即时编译器(JIT)也会做指令重排。
可想而知,如果任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题。
并发编程会带来什么问题
后面说的和硬件无关的概念关注我的可能听得都有点懵逼,应该大多数都是软件工程师吧,然而对于并发编程的问题咱们应该是有所理解的,比方耳熟能详的原子性问题,可见性问题和有序性问题啊。
其实呢,原子性问题,可见性问题和有序性问题是前面初代程序大佬们形象进去的概念,对应的便是后面提到的缓存一致性问题、处理器优化问题和指令重排问题等,不得不说,初代程序大佬们为了让咱们这群软件工程师可能了解硬件的概念也是殚精竭虑了。
并发编程为了保证数据的平安,必须满足以下三个个性:
- 原子性,指的是在一个操作中 CPU 不能够在中途暂停而后再调度,要么不执行,要么就执行实现。
- 可见性,指的是多个线程拜访同一个变量时,一个线程批改了这个变量的值,其余线程可能立刻看失去批改后的值。
- 有序性,指的是程序执行的程序依照代码的先后顺序执行,而不能瞎几把重排,导致程序呈现不统一的后果。
看完下面的三个个性的解释,咱们也能晓得,缓存一致性问题其实就是可见性问题,而处理器优化是能够导致原子性问题的,指令重排即会导致有序性问题。
总结下来就是:并发编程会带来原子性问题、可见性问题、有序性问题
什么是内存模型
下面说到了缓存一致性问题,其实是硬件的一直降级导致的,有些心大的敌人可能就会间接说了,破除处理器和处理器的优化技术、破除 CPU 缓存,让 CPU 间接和主存交互不就没问题了吗?
首先,想法是必定的,能够解决,然而做法就有点过了,相当于为了防止有车祸产生,间接将汽车废除掉一样。
Java 为了保障并发编程中能够满足原子性、可见性及有序性,诞生出了一个重要的概念,那就是内存模型,内存模型定义了共享内存零碎中多线程程序读写操作行为的标准。
通过这些规定来标准对内存的读写操作,从而保障指令执行的正确性,它解决了 CPU 多级缓存、处理器优化、指令重排等导致的内存拜访问题,保障了并发场景下的一致性、原子性和有序性。
总结下来就是:Java 内存模型定义了共享内存零碎中多线程程序读写操作行为的标准,Java 内存模型也就是为了解决这个并发编程问题而存在的。
内存模型怎么解决并发问题的?
内存模型解决并发问题次要采取两种形式,别离是限度处理器优化,另一种是应用了内存屏障。
而对于这两种形式,Java 底层其实曾经封装好了一些关键字,咱们这边只须要用起来就能够了。
对于解决并发编程中的原子性问题,Java 底层封装了 Synchronized 的形式,来保障办法和代码块内的操作都是原子性的;
而至于可见性问题,Java 底层则封装了 Volatile 的形式,将被润饰的变量在批改后立刻同步到主内存中。
至于有序性问题,其实也就是咱们所说的重排序问题,Volatile 关键字也会禁止指令的重排序,而 Synchroinzed 关键字因为保障了同一时刻只容许一条线程操作,天然也就保障了有序性。
总结一波:看到这里,基本上都应该理解 JMM 是什么以及用来干嘛的了吧,下面的解释应该是很清晰易懂的了,如果还看不懂就看两遍吧,了解 JMM 是什么对并发编程来说太重要了。
分享几个常考的面试题
说说看线程之间的通信机制有哪些呢?Java 的并发采纳的是哪种?
线程之间的通信机制能够分为两种,别离是
- 共享内存
- 消息传递
目前 Java 的并发通信采纳的是共享内存的形式。
这道题算是比拟常见的实践题了,有一部分人对操作系统有点理解的晓得线程之间的通信机制有两种,然而很少人晓得 Java 的并发通信采纳的是共享内存的形式。
恩不错,能晓得 Java 线程通过共享内存的形式进行通信是了解内存模型的第一步,说说看你对内存模型的了解吧?
内存模型是吗?请问下是 JVM 的内存模型呢?还是 Java 内存模型,也就是 JMM 呢?
这个问题比拟容易让人混同,大多数一听内存模型,都会率先想到 JVM 内存模型,也就是堆内存那些,所以如果遇见概念不清的问题,肯定要大胆问,记得我刚毕业的时候第一次听见这种问题,就了解错认为是 JVM 内存模型导致丢分。
是 Java 内存模型哈,也就是 JMM,你说说看什么是内存模型?有什么存在作用?
JMM 其实并不像 JVM 内存模型一样是实在存在的,它只是一个形象的标准。在不同的硬件或者操作系统下,对内存的拜访逻辑都有肯定的差别,而这种差别会导致同一套代码在不同操作系统或者硬件下,失去了不同的后果,而 JMM 的存在就是为了解决这个问题,通过 JMM 的标准,保障 Java 程序在各种平台下对内存的拜访都能失去统一的成果。
JMM 的概念其实比拟容易遗记,所以我这边顺便表明了它是为了解决下面问题而存在的,通过了解它是什么,用来做什么,比拟容易产生深度记忆。
嗯,不错,说说 JMM 对内存的划分?
JMM 规定了内存次要划分为主内存和工作内存两种,规定所有的变量都存储在主内存中,每条线程还有本人的工作内存,线程的工作内存中保留了该线程中用到的变量的主内存的正本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能间接读写主内存。
不同的线程之间也无奈间接拜访对方工作内存中的变量,线程间变量的传递均须要本人的工作内存和主存之间进行数据同步进行。
为了分明展现这个过程,我顺便画了张图
图画的不错,你这些工作内存和主内存指的是啥?
此处的工作内存和主内存其实跟 JVM 内存的划分是在不同档次上进行的,是本人的一套抽象概念,大略能够了解为,主内存对应的是 Java 堆中的对象实例局部,而工作内存对应的则是栈中的局部区域,
说说 JMM 定义了哪些操作来实现主内存和工作内存的交互操作?
JMM 定义了 8 个操作来实现主内存和工作内存的交互操作,首先是从 lock 加锁开始,把主内存中的变量标记为一条线程独占的状态;read 读取,将一个变量的值从主内存传输到工作内存中;load 加载,把 read 失去的值加载到工作内存的变量正本中;use 应用,把工作内存中变量的值传递给执行引擎;assign 赋值,把从执行引擎接管到的值赋值给工作内存的变量;store 存储,把工作内存中变量的值传送回主内存中;write 写入,把 store 失去的值放入主内存的变量中;最初是 unlock 解锁,把主内存中处于锁定状态的变量释放出来,流程到这一步就完结了。
能够,流程比拟清晰,说说对内存交互基本操作的三个个性的了解?
JMM 根本能够说是围绕着在并发中如何解决这三个个性而建设起来的,也就是原子性、可见性、以及有序性。
所谓的原子性指的就是一个操作或者多个操作要么全副执行并且执行的过程不会被任何因素打断,要么就都不执行。
拓展: 咱们都晓得 CPU 有工夫片的概念,会依据不同的调度算法进行线程调度,而当线程在执行一个读改写操作时,在执行完读改之后,工夫片耗完,就会被要求放弃 CPU,并期待从新调度。这种状况下,读改写就不是一个原子操作,即存在原子性问题。
可见性是指当多个线程拜访同一个变量时,一个线程批改了这个变量的值,其余线程可能立刻看失去批改的值。
拓展: 多个线程拜访过程中的某个共享内存时,这多个线程是别离在不同的 CPU 上执行的,则每个 CPU 都会在各自的 cache 中保留一份共享内存的缓冲,因为多核是能够并行的,可能会呈现多个线程同时写各自的缓存的状况,而各自的 cache 之间的数据就有可能不同,这就存在了可见性问题。
有序性即程序执行的程序依照代码的先后顺序执行。
拓展: 因为处理器优化和指令重排以及 CPU 还可能对输出代码进行乱序执行,比方 load->add->save 有可能被优化成 load->save->add,这就是有序性问题。
归根结底,就是为了实现多个线程的工作内存的数据一致性,让程序在多线程并发、指令重排序优化的环境中也能如预期中的一样执行。
大白话解释了一波原子性问题、可见性问题、有序性问题,同样先记住概念,再了解拓展,够了,没什么坑。概念那块是必答的,而拓展那块其实是加分项,咱们面试官都喜爱候选人对答案有本人的见解,而不是一上来就是背各种答案,没有本人的见解只能算是平庸。
你看看,以下几种状况中,哪几个操作是原子性操作?
a = 20;
b = a;
除了第一个操作,其余都是非原子性操作。
这里可能很多人都不了解,为什么第二个操作是非原子性操作,实际上第二个操作蕴含了两局部,它先要去读取 a 的值,而后再讲 a 的值写入 b 中,尽管读取 a 的值以及将 a 的值写入工作内存都是两个原子性操作,然而合起来就不是原子性操作了。
不错,那 java 如何来保障原子性操作呢?
JMM 只保障了根本读取和赋值是原子性的操作,然而如果要实现更大范畴操作的原子性,则能够通过 synchroinzed 和 lock 来实现,synchronized 和 lock 可能保障任一时刻只有一个线程执行该代码块,从而保障了原子性。
你说说看 java 用什么来保障可见性的?
对可见性来说,Java 提供了 volatile 关键字来保障可见性,而 synchronized 和 lock 也可能保障可见性,synchronized 和 lock 能保障同一时刻只有一个线程获取锁而后执行同步代码,并且在开释锁之前会将对变量的批改刷新到主存当中,因而能够保障可见性。
说说看 volatile 如何失效的?
当一个共享变量被 volatile 润饰时,它会保障批改的值被立刻更新到主内存中,当有其余线程读取该值时,也不会间接读取工作内存中的值,而是间接去主内存中读取。
而一般的共享变量不能保障可见性的,因为一般共享变量被批改后,写写入了工作内存中,什么时候写入主内存其实是不可知的,当其余线程去读取是,此时无论是工作内存还是主内存,可能还是原来的值,因而无奈保障可见性。
说说看 Java 是如何保障有序性的?
首先,Java 里边能够通过 synchronized 和 lock 来保障有序性,synchronized 和 Lock 能够保障每个时刻是有一个线程执行同步代码,相当于是让线程依照程序的执行同步代码,天然也就保障了有序性。
另外 Java 内存模型也通过 happens-before 准则来保障有序性。
不错,对三个个性的了解有独到之处,你刚刚说到 happens-before 准则,说说看对它的了解?
这个关比较复杂,也不好形容,为了向面试官分明形容这个过程,我举了一个例子,退出当初程序中有两个操作,别离为 A 和 B;
首先这两个操作能够在一个线程之内被执行,也能够在不同线程之间被执行;
而如果是单线程下的话,编译后的字节码人造就包含了 happens-before 关系,因为单线程内共享一份工作内存,不存在数据一致性的问题。在程序控制流门路中靠前的字节码 happens-before 靠后的字节码,即靠前的字节码执行完之后操作后果对靠后的字节码是可见的。
当然了,这并不意味着前者肯定在后者之前执行,实际上,如果后者不依赖前者的运行后果,那么它们可能会被重排序。
而如果是多线程下的话,因为每个线程都有一份共享变量的正本,如果没有对共享变量做同步的解决,线程 1 更新执行操作 A 共享变量的值之后,线程 2 开始执行操作 B,此时操作 A 产生的后果对操作 B 不肯定可见。
为了解决这个多线程开发的问题,不便程序开发,JMM 通过 happens-before 关系向咱们程序员提供跨线程的内存可见性保障,也就是说如果线程 1 的 A 操作与线程 2 的 B 操作之间存在 happens-before 关系,只管 A 操作和 B 操作在不同的线程中执行,JMM 仍旧向咱们程序员保障 A 操作对 B 操作是可见的。
嗯,依照你的答复,JMM 的存在同时限度了重排序吗?
不是的。JMM 尽管定义了如果一个操作 happens-before 另一个操作,那么第一个操作的执行后果将对第二个操作可见,然而这并不是认为这 Java 平台的具体实现必须依照 happens-before 关系指定的程序执行,如果重排序之后的执行后果,与按 happens-before 关系来执行的后果统一,那么这种重排序并不非法,也就是说,JMM 是容许这种重排序的。
扩大 :咱们要记住,JMM 其实就是是在遵循一个根本准则,就是只有不改变程序的执行后果,不论是单线程,还是多线程,编译器和处理器怎么优化都行。其实 JMM 能够这么做的起因也很简略,毕竟咱们开发中对于这两个操作是否真的被重排序并不关怀,咱们关怀的是程序执行后果不能被扭转,也就是只有别出 bug 就能够了,哭唧唧。
最初
目前好好面试系列曾经汇总了以下面试题
- 暗藏在 Java 根底中的 50 个坑
- 全面理解 JMM 以及常考面试题
- …
后续会持续推出 JVM、汇合、Spring 系列,有趣味的关注一波。
为了感激最近大家的反对,我这边顺便跳了一些 Java 相干的资源,很多专题,比方 JAVA+TCP、Java 反射机制、Java 多线程专题等,都是针对性训练的利器,倡议人手一份。
有趣味的关注我一波,Java 面试官带你们跨过一个个的面试坑,保障不亏。
公众号:饭谈编程
原文链接:https://mp.weixin.qq.com/s/_zmhLhEDgLggejUdF1c9gw
谢谢点赞反对????????????!