关于java:面试官为什么需要Java内存模型

7次阅读

共计 4146 个字符,预计需要花费 11 分钟才能阅读完成。

面试官 明天想跟你聊聊 Java 内存模型,这块你理解过吗?

候选者:嗯,我简略说下我的了解吧。那我就从为什么要有 Java 内存模型开始讲起吧

面试官:开始你的表演吧。

候选者:那我先说下背景吧

候选者 :1. 现有计算机往往是多核的,每个外围下会有高速缓存。高速缓存的诞生是因为「CPU 与内存(主存) 的速度存在差别」,L1 和 L2 缓存个别是「每个外围独占」一份的。

候选者:2. 为了让 CPU 进步运算效率,处理器可能会对输出的代码进行「乱序执行」,也就是所谓的「指令重排序」

候选者:3. 一次对数值的批改操作往往是非原子性的(比方 i ++ 实际上在计算机执行时就会分成多个指令)

候选者:在永远单线程下,下面所讲的均不会存在什么问题,因为单线程意味着无并发。并且在单线程下,编译器 /runtime/ 处理器都必须恪守 as-if-serial 语义,恪守 as-if-serial 意味着它们不会对「数据依赖关系的操作」做重排序。

候选者:CPU 为了效率,有了高速缓存、有了指令重排序等等,整块架构都变得复杂了。咱们写的程序必定也想要「充沛」利用 CPU 的资源啊!于是乎,咱们应用起了多线程

候选者:多线程在意味着并发,并发就意味着咱们须要思考线程平安问题

候选者:1. 缓存数据不统一:多个线程同时批改「共享变量」,CPU 外围下的高速缓存是「不共享」的,那多个 cache 与内存之间的数据同步该怎么做?

候选者:2. CPU 指令重排序在多线程下会导致代码在非预期下执行,最终会导致后果存在谬误的状况。

候选者:针对于「缓存不统一」问题,CPU 也有其解决办法,常被大家所意识的有两种:

候选者:1. 应用「总线锁」:某个外围在批改数据的过程中,其余外围均无奈批改内存中的数据。(相似于独占内存的概念,只有有 CPU 在批改,那别的 CPU 就得期待以后 CPU 开释)

候选者:2. 缓存一致性协定(MESI 协定,其实协定有很多,只是举个大家都可能见过的)。MESI 拆开英文是(Modified(批改状态)、Exclusive(独占状态)、Share(共享状态)、Invalid(有效状态))

候选者:缓存一致性协定我认为能够了解为「缓存锁」,它针对的是「缓存行」(Cache line) 进行”加锁”,所谓「缓存行」其实就是 高速缓存 存储的最小单位。

面试官:嗯…

候选者:MESI 协定的原理大略就是:当每个 CPU 读取共享变量之前,会先辨认数据的「对象状态」(是批改、还是共享、还是独占、还是有效)。

候选者:如果是独占,阐明以后 CPU 将要失去的变量数据是最新的,没有被其余 CPU 所同时读取

候选者:如果是共享,阐明以后 CPU 将要失去的变量数据还是最新的,有其余的 CPU 在同时读取,但还没被批改

候选者 :如果是批改,阐明以后 CPU 正在批改该变量的值,同时会向其余 CPU 发送该数据状态为 invalid(有效) 的告诉,失去其余 CPU 响应后(其余 CPU 将数据状态从共享 (share) 变成 invalid(有效)),会以后 CPU 将高速缓存的数据写到主存,并把本人的状态从 modify(批改)变成 exclusive(独占)

候选者:如果是有效,阐明以后数据是被改过了,须要从主存从新读取最新的数据。

候选者:其实 MESI 协定做的就是判断「对象状态」,依据「对象状态」做不同的策略。要害就在于某个 CPU 在对数据进行批改时,须要「同步」告诉其余 CPU,示意这个数据被我批改了,你们不能用了。

候选者:比拟于「总线锁」,MESI 协定的”锁粒度”更小了,性能那必定会更高咯

面试官 但据我理解,CPU 还有优化,你还晓得吗?

候选者:嗯,还是理解那么一点点的。

候选者 :从后面讲到的,能够发现的是:当 CPU 批改数据时,须要「同步」通知其余的 CPU,期待其余 CPU 响应接管到 invalid(有效) 后,它能力将高速缓存数据写到主存。

候选者:同步,意味着期待,期待意味着什么都干不了。CPU 必定不乐意啊,所以又优化了一把。

候选者:优化思路就是从「同步」变成「异步」。

候选者:在批改时会「同步」通知其余 CPU,而当初则把最新批改的值写到「store buffer」中,并告诉其余 CPU 记得要改状态,随后 CPU 就间接返回干其余事了。等到收到其它 CPU 发过来的响应音讯,再将数据更新到高速缓存中。

候选者 :其余 CPU 接管到 invalid(有效) 告诉时,也会把接管到的音讯放入「invalid queue」中,只有写到「invalid queue」就会间接返回通知批改数据的 CPU 曾经将状态置为「invalid」

候选者:而异步又会带来新问题:那我当初 CPU 批改完 A 值,写到「store buffer」了,CPU 就能够干其余事了。那如果该 CPU 又接管指令须要批改 A 值,但上一次批改的值还在「store buffer」中呢,没批改至高速缓存呢。

候选者:所以 CPU 在读取的时候,须要去「store buffer」看看存不存在,存在则间接取,不存在才读主存的数据。【Store Forwarding】

候选者:好了,解决掉第一个异步带来的问题了。(雷同的外围对数据进行读写,因为异步,很可能会导致第二次读取的还是旧值,所以首先读「store buffer」。

面试官 还有其余?

候选者:那当然啊,那「异步化」会导致雷同外围读写共享变量有问题,那当然也会导致「不同」外围读写共享变量有问题啊

候选者 :CPU1 批改了 A 值,已把批改后值写到「store buffer」并告诉 CPU2 对该值进行 invalid(有效) 操作,而 CPU2 可能还没收到 invalid(有效)告诉,就去做了其余的操作,导致 CPU2 读到的还是旧值。

候选者 :即使 CPU2 收到了 invalid(有效) 告诉,但 CPU1 的值还没写到主存,那 CPU2 再次向主存读取的时候,还是旧值…

候选者:变量之间很多时候是具备「相关性」(a=1;b=0;b=a),这对于 CPU 又是无感知的…

候选者:总体而言,因为 CPU 对「缓存一致性协定」进行的异步优化「store buffer」「invalid queue」,很可能导致前面的指令很可能查不到后面指令的执行后果(各个指令的执行程序非代码执行程序),这种景象很多时候被称作「CPU 乱序执行」

候选者:为了解决乱序问题(也能够了解为可见性问题,批改完没有及时同步到其余的 CPU),又引出了「内存屏障」的概念。

面试官:嗯…

候选者:「内存屏障」其实就是为了解决「异步优化」导致「CPU 乱序执行」/「缓存不及时可见」的问题,那怎么解决的呢?嗯,就是把「异步优化」给”禁用“掉(:

候选者:内存屏障能够分为三种类型:写屏障,读屏障以及全能屏障(蕴含了读写屏障),屏障能够简略了解为:在操作数据的时候,往数据插入一条”非凡的指令”。只有遇到这条指令,那后面的操作都得「实现」。

候选者:那写屏障就能够这样了解:CPU 当发现写屏障的指令时,会把该指令「之前」存在于「store Buffer」所有写指令刷入高速缓存。

候选者:通过这种形式就能够让 CPU 批改的数据能够马上裸露给其余 CPU,达到「写操作」可见性的成果。

候选者:那读屏障也是相似的:CPU 当发现读屏障的指令时,会把该指令「之前」存在于「invalid queue」所有的指令都解决掉

候选者:通过这种形式就能够确保以后 CPU 的缓存状态是精确的,达到「读操作」肯定是读取最新的成果。

候选者:因为不同 CPU 架构的缓存体系不一样、缓存一致性协定不一样、重排序的策略不一样、所提供的内存屏障指令也有差别,为了简化 Java 开发人员的工作。Java 封装了一套标准,这套标准就是「Java 内存模型」

候选者:再具体地说,「Java 内存模型」心愿 屏蔽各种硬件和操作系统的拜访差别,保障了 Java 程序在各种平台下对内存的拜访都能失去统一成果。目标是解决多线程存在的原子性、可见性(缓存一致性)以及有序性问题。

面试官 那要不简略聊聊 Java 内存模型的标准和内容吧?

候选者:不了,怕一聊就是一个下午,下次吧?

本文总结

  • 并发问题产生的三大本源是「可见性」「有序性」「原子性」
  • 可见性:CPU 架构下存在高速缓存,每个外围下的 L1/L2 高速缓存不共享(不可见)
  • 有序性:次要有三方面可能导致突破

    • 编译器优化导致重排序(编译器能够在不扭转单线程程序语义的状况下,能够对代码语句程序进行调整从新排序)
    • 指令集并行重排序(CPU 原生就有可能将指令进行重排)
    • 内存零碎重排序(CPU 架构下很可能有 store buffer /invalid queue 缓冲区,这种「异步」很可能会导致指令重排)
  • 原子性:Java 的一条语句往往须要多条 CPU 指令实现(i++),因为操作系统的线程切换很可能导致 i++ 操作未实现,其余线程“中途”操作了共享变量 i,导致最终后果并非咱们所期待的。
  • 在 CPU 层级下,为了解决「缓存一致性」问题,有相干的“锁”来保障,比方“总线锁”和“缓存锁”。

    • 总线锁是锁总线,对共享变量的批改在雷同的时刻只容许一个 CPU 操作。
    • 缓存锁是锁缓存行 (cache line),其中比拟闻名的是 MESI 协定,对缓存行标记状态,通过“同步告诉”的形式,来实现(缓存行) 数据的可见性和有序性
    • 但“同步告诉”会影响性能,所以会有内存缓冲区 (store buffer/invalid queue) 来实现「异步」进而进步 CPU 的工作效率
    • 引入了内存缓冲区后,又会存在「可见性」和「有序性」的问题,素日大多数状况下是能够享受「异步」带来的益处的,但多数状况下,须要强「可见性」和「有序性」,只能”禁用”缓存的优化。
    • “禁用”缓存优化在 CPU 层面下有「内存屏障」,读屏障 / 写屏障 / 全能屏障,实质上是插入一条”屏障指令”,使得缓冲区 (store buffer/invalid queue) 在屏障指令之前的操作均已被解决,进而达到 读写 在 CPU 层面上是可见和有序的。
  • 不同的 CPU 实现的架构和优化均不一样,Java 为了屏蔽硬件和操作系统拜访内存的各种差别,提出了「Java 内存模型」的标准,保障了 Java 程序在各种平台下对内存的拜访都能失去统一成果

欢送关注我的微信公众号【Java3y】来聊聊 Java 面试,对线面试官系列继续更新中!

【对线面试官 - 挪动端】系列 一周两篇继续更新中!

【对线面试官 - 电脑端】系列 一周两篇继续更新中!

原创不易!!求三连!!

正文完
 0