面试官明天想跟你聊聊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面试,对线面试官系列继续更新中!

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

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

原创不易!!求三连!!