当前位置: 首页 > 后端技术 > Java

面试官:为什么需要Java内存模型?

时间:2023-04-01 16:54:08 Java

面试官:今天想跟大家聊聊Java内存模型。你明白了吗?应试者:那我简单说一下我的理解。然后我将从为什么有Java内存模型开始。采访者:开始你的表演吧。应聘者:我先说说背景。考生:1.现有的电脑往往都是多核的,每个核下都会有一个缓存。缓存的诞生是因为“CPU和内存(主存)的速度存在差异”,而L1和L2缓存一般都是“每个核心独享”的。考生:2、CPU为了提高运算效率,处理器可能会对输入的代码进行“乱序执行”,即所谓的“指令重排序”。考生:3.一个值的修改往往是非原子性的(比如i++在计算机执行的时候其实是分成多条指令)考生:在永恒单线程下,不会有上面的问题,因为单线程意味着没有并发。在单线程下,编译器/运行时/处理器必须遵守似串行语义。遵守as-if-serial意味着他们不会重新排序“数据相关操作”。考生:为了效率,CPU有了缓存,指令重排序等等,整个架构变得复杂了。我们写的程序肯定也想“充分”利用CPU资源!于是乎,我们使用了多线程候选:多线程意味着并发,并发意味着我们需要考虑线程安全问题。考生:1、缓存数据不一致:多个线程同时修改“共享变量”,CPU核心下面的缓存是“不共享”的,那么多个缓存和内存之间的数据同步怎么办呢?考生:2、多线程下CPU指令重排序会导致代码执行异常,最终导致结果出错。考生:对于“缓存不一致”的问题,CPU也有它的解决办法。经常被大家认可的有两种:候选:1.使用“总线锁”:当一个核正在修改数据时,其他核都不能修改内存中的数据。(类似于独占内存的概念,只要一个CPU在修改,其他CPU就得等待当前CPU释放)候选:2.缓存一致性协议(MESI协议,其实有很多协议,但是每个人都可以看到它通过了)。MESI英文反汇编是(Modified(修改状态),Exclusive(独占状态),Share(共享状态),Invalid(无效状态))候选:我觉得缓存一致性协议可以理解为“缓存锁”,目的是在“锁定”的是“缓存行”(Cacheline)。所谓“缓存行”,其实就是缓存存储的最小单位。面试官:嗯……考生:MESI协议的原理大致是:每个CPU在读取一个共享变量之前,首先会识别数据的“对象状态”(是否被修改、共享、独占、无效).候选:如果是exclusive,说明当前CPU要获取的变量数据是最新的,没有同时被其他CPU读取过。Candidate:如果是共享的,说明当前CPU要获取的变量数据还是最新的,有其他CPU同时读取,但是没有被修改Candidate:如果是修改的,说明当前CPU正在修改变量的值,同时会向其他CPU发送数据状态无效的通知。收到其他CPU的响应后(其他CPU将数据状态由共享(share)变为无效(invalid),当前CPU将缓存数据写入主存,状态由修改(modified)变为独占(exclusive)candidate:如果无效,说明当前数据已经改变,需要重新从主存中读取最新的数据。candidate:其实MESI协议做的就是判断“对象状态””,根据“对象状态”执行不同的策略。关键是当一个CPU修改数据时,需要“同步”通知其他CPU,说明这个数据已经被我修改了,你不能再使用了考生:MESI协议的“锁粒度”相对于“总线锁”更小,性能肯定会更高面试官:但据我所知,CPU还是有优化的,你知道吗?考生:嗯,还是有点明白了考生:从上面可以发现,CPU修改数据的时候,需要“同步”告诉其他CPU,等待其他CPU响应接收无效(无效),然后才能将缓存数据实时写入主服务器。考生:同步就是等待,等待就是什么都不做。CPU肯定不高兴,所以又优化了一遍。考生:优化的思路是从“同步”变成“异步”。候选:修改的时候会“同步”告诉其他CPU,但是现在是把最新修改的值写入“storebuffer”,通知其他CPU记住要改变状态,然后CPU直接返回做其他事物。等到收到其他CPU发来的响应报文,再更新数据到缓存中。考生:当其他CPU收到无效(invalid)通知时,也会将收到的消息放入“无效队列”。只要写入“无效队列”,就会直接返回告诉修改数据的CPU,状态已经设置为“无效”的候选:异步会带来新的问题:既然CPU已经修改了A的值写入“storebuffer”后,CPU就可以做其他事情了。那么如果CPU收到一条指令,需要修改A的值,但是最后修改的值还在“storebuffer”中,没有修改到缓存中。考生:因此,CPU在读的时候,需要去“storebuffer”看看是否存在。如果存在则直接取,如果不存在则读取主存中的数据。[StoreForwarding]考生:嗯,第一个异步问题已经解决了。(同一个核读写数据,由于异步,很有可能第二次读到的还是旧值,所以先读“storebuffer”。面试官:还有别的吗?考生:当然,然后是“异步”"会导致同核的共享变量读写出现问题,当然也会导致"不同"核的共享变量读写出现问题候选:CPU1修改了A的值,并写入了修改后的值到“storebuffer”并通知CPU2对该值进行无效(invalid)操作,而CPU2可能在收到无效(invalid)通知之前已经做了其他操作,导致CPU2读到旧值。候选:即使CPU2收到invalid(无效)通知,但是CPU1的值还没有写入主存,那么当CPU2再次从主存读取时,还是旧值...考生:变量之间往往存在“相关性”(a=1;b=0;b=a),对CPU不敏感...考生:一般来说,由于CPU对“缓存一致性协议”“storebuffer”和“无效队列”的异步优化,很可能导致后面的指令可能找不到前面指令的执行结果(每条指令的执行顺序不是代码执行顺序),这种现象常被称为“CPUout-of-orderexecution”候选:为了解决乱序问题(也可以理解为可见性问题,修改没有及时同步到其他CPU),由此引出“内存屏障”的概念.面试官:嗯……考生:“内存屏障”其实就是为了解决“异步优化”导致的“CPU乱序执行”/“缓存不及时可见”的问题,那么怎么解决呢?嗯,就是“禁用”“异步优化”(:考生:内存屏障分为三种:写屏障、读屏障和万能屏障(包括读写屏障),屏障可以简单理解为:在操作时数据,在数据中插入一条“特殊指令”,只要遇到这条指令,前面的操作肯定是“完成”的。考生:写屏障可以这样理解:当CPU找到写屏障指令时“storeBuffer”中存在的“before”指令,所有的写指令都会被flush到缓存中。候选:这样,CPU修改的数据可以立即暴露给其他CPU,这样“写操作”就可以可见考生:读屏障也类似:当CPU找到一条读屏障的指令时,会处理该指令“之前”存在于“无效队列”中的所有指令。考生:这样,就可以确保当前CPU的缓存状态是准确的,要实现“读取操作”必须是读取最新的效果。考生:由于不同CPU架构的缓存系统不同,缓存一致性协议不同,重排序策略不同,提供的内存屏障指令也不同,为了简化Java开发者的工作。各种硬件和操作系统之间的访问差异保证了Java程序在各种平台上对内存访问可以获得一致的效果。目的是解决多线程的原子性、可见性(缓存一致性)和排序问题。面试官:为什么不简单说一下Java内存模型的规范和内容呢?应聘者:不,恐怕我们会花一个下午的时间聊天,下次吧?本文总结:导致并发问题的三大原因是“可见性”、“有序性”和“原子性”。可见性:CPU架构下有缓存,每个核心下的L1/L2缓存不共享(不可见)并在不改变单线程程序语义的情况下对代码语句的顺序进行重新排序)指令集并行重新排序(CPU本机有可能重新排列指令)内存系统重新排序(CPU下很可能存在存储缓冲区/无效队列缓冲区体系结构上,这种“异步”很可能会造成指令重排)原子性:Java中的一条语句往往需要多条CPU指令完成(i++)。由于操作系统的线程切换,i++操作可能没有完成,其他线程“中途”操作共享变量i,导致最终结果不是我们预期的。在CPU层面,为了解决“缓存一致性”的问题,有相关的“锁”来保证,比如“总线锁”、“缓存锁”。总线锁就是对总线进行锁定,共享变量的修改只允许一个CPU同时操作。缓存锁是对缓存行(cacheline)进行加锁,其中比较有名的是MESI协议,通过“同步通知”的方式标记缓存行的状态,实现(缓存行)数据的可见性和有序性”,但是“同步通知”会影响性能,所以会有内存缓冲区(storebuffer/invalidqueue)实现“异步”,提高CPU工作效率。引入内存缓冲区后,就会有“可见性”而“排序”在大多数情况下,可以享受到“异步”的好处,但在少数情况下,需要很强的“可见性”和“排序”,只能“禁用”缓存的优化。“禁用”》缓存优化在CPU层面有“内存屏障”,readbarrier/writebarrier/universalbarrier,本质上是插入了一个“屏障指令”,让缓冲区(storebuffer/invalidqueue)操作在屏障指令之前都已经处理好了所以读写在CPU级别可见且有序。不同的CPU实现有不同的架构和优化。为了屏蔽硬件和操作系统对内存访问的各种差异,Java提出了“Java内存模型”规范,保证了Java程序可以在各种平台上访问内存。如果你能得到一致的结果,欢迎关注我的微信公众号【Java3y】聊聊Java面试,在线面试官系列持续更新中!【在线面试官-手机版】系列,每周两篇,持续更新中!【在线面试官-电脑】系列每周两篇持续更新中!原创不易!!一连求三!!