豁然开朗:这问题我不信你能分析的这么透彻!

2023年 10月 9日 34.8k 0

  • 本章难度:★★☆☆☆
  • 本章重点:掌握多个线程同时读写同一共享变量存在共享问题的根本原因,重点掌握CPU内存模式和Java内存模型的核心原理,缓存一致性问题及其产生的原因,并能够根据CPU内存模型和Java内存模型编写线程安全的代码。

大家好,我是冰河~~

“原来如此啊,真没想到统计个调用商品详情接口次数的功能背后还会牵扯到这么多知识点,这些知识之前确实没听说过,看来确实有很多我之前不知道的东西呀,以后跟着老大好好学,争取跟他一样厉害”,小菜默默的在心里嘀咕着。

一、情景再现

小菜凭借着之前在学校的传奇经历顺利进入一家头部互联网公司实习,几天后,被分配到一个统计线上调用商品详情接口的任务,本以为很简单的功能,小菜也三下五除二的完成了开发任务,但是在测试时,却被告知小菜统计出来的结果和实际结果差距太大。经过一天的排查和定位,小菜最终也没有找出问题出在哪里。

二、寻求帮助

第二天,小菜早早来到公司,还在思考着昨天为什么自己写的代码明明看起来没问题,却跟实际统计结果差距这么大。

正当小菜还在纠结时,他突然听到:“小菜,怎么样了,知道昨天为什么会出现问题了吗?”。

小菜转过头一看,原来是自己的直属领导老王到公司了,“没有呀,我昨天下班后也在想这个问题,但是还是没找到原因”。

此时,小菜起身来到老王的工位旁边,“老大,昨天我搞到很晚也没发现啥问题,你可以给我讲讲哪里出了问题吗?”。

“可以”,老王一边说着,一边从电脑包里拿出自己的电脑。“其实,要搞清楚为啥昨天你写的代码会出问题,这就涉及到内存模型了,说到内存模型,就要从CPU内存模型和Java内存模型两个方面进行讲述了”。

“这样吧,小菜拿上笔和本,现在会议室没人使用,我单独给你讲讲”。

“好的”,小菜边说,边回到工位拿笔和本子,老王则拿起了自己的电脑,二人一起到会议室走去。

三、CPU内存模型

“以前了解过CPU内存模型吗?”。

“在学校听老师讲过,不过讲的不够深入和具体,我也了解的不多,我确实也想不出来这跟昨天实现的功能有啥关系”。

“没关系,我今天给你讲一下”,老王边说,边打开了电脑和投影仪。

“你昨天写的代码出问题,本质上与内存模型有关,说到内存模型呢,又会涉及到CPU内存模型和Java内存模型,我们先来讲讲CPU内存模型吧”,老王将自己的电脑投到投影仪上。

“好的”,小菜边听边在本子上记。

老王接着说:“Java程序一般都是运行在JVM上,JVM本身有自己的内存模型,Java的内存模型其实与CPU的内存模型有很多相似之处。如果是CPU内存模型呢,计算机执行程序时,每条执行指令都是在CPU中执行的,并且在CPU执行指令的过程中就会涉及到数据的读写操作,CPU并不会直接从计算机的磁盘上读数据,而是从计算机的主存,也就是我们常说的内存中读取数据,并且CPU也会将处理的结果数据写回主存”。

老王一边说,一边在脑电上画出了这样一张图,如图2-1所示。

图片图片

“你在大学里应该学到过,其实CPU的执行速度是非常快的,会比内存的读写速度快的多”,老王画完图说道。

“是的,这个我了解过”,小菜回应道。

“CPU的执行速度和内存的读写速度存在巨大的速度差,这样就会存在一个问题,由于CPU在处理任务时,需要从内存中读取数据,内存的读写速度远远不及CPU的执行速度,这样就会导致CPU的执行速度大大下降”。

“为了解决这个问题,CPU的架构师们在CPU内部设计了一个高速缓冲区,用来平缓CPU执行速度与内存读写速度之间的差距。在执行时,会将CPU执行任务要读取的数据从内存读取到CPU的告诉缓冲区,然后CPU再从高速缓冲区读取数据后执行任务。当CPU执行完任务,也是先将结果数据写回到高速缓冲区,随后再将高速缓冲区的数据刷新到内存,这样CPU的执行效率就大大提升了,我们再来画一张图”。

说完,老王又画了一张图,如图2-2所示。

图片图片

“你可以记一下这张图,这里咱们也暂时不展开讲,如果展开讲的话,会涉及到很多的知识点,比如CPU多级缓存架构,缓存一致性,伪共享,内存屏障等等很多知识点,一时半会儿也讲不完,如果今天都讲了的话,我估计你可能也消化不完,所以,关于CPU内存模型,今天就讲到这里”。

“好的”,小菜边听,边拿本子记下了这张图。

“如果你对CPU多级缓存架构,缓存一致性,伪共享,内存屏障等等这些知识点感兴趣,想进一步学习的话,我给你推荐一本书,就是冰河写的《深入理解高并发编程:核心原理与案例实战》这本书,这本书剖析了并发编程出现各种诡异Bug问题的根源,从本质上深度解析了并发编程的核心原理,并且给出了很实用的实战案例,吃透这本书,对你学习并发编程帮助很大,这本书在京东和当当都在售”。老王继续说道。

“好的”,小菜也记下了这本书的书名,准备入手一本。

“那我们再来讲讲Java内存模型”。

四、Java内存模型

“说起Java内存模型,其实与CPU内存模型有很多相似的地方,只是说Java内存模型中屏蔽了不同操作系统和底层硬件之间的访问差异,能够在不同的操作系统和底层硬件之间达到一致的访问效果”,老王一边说,一边画图,画了一张线程、主内存、工作内存的关系图,如图2-3所示。

图片图片

“Java内存模型规定了所有的变量都存储在主内存中,也就是存储在计算机的物理内存中,每个线程都有自己的工作内存,用于存储线程私有的数据,线程对变量的所有操作都需要在工作内存中完成。一个线程不能直接访问其他线程工作内存中的数据,只能通过主内存进行数据交互。也就是说,线程在执行任务时,会先将数据从主内存复制到自己的工作内存,然后执行对应的任务,任务执行完毕后,会将计算的结果数据,从自己的工作内存刷新到主内存,这就与CPU内存模型很相似了”。

老王边说边喝了口水,继续道:“你先消化下,CPU内存模型和Java内存模型,接下来,我们再讲讲你昨天写的代码为啥会出问题”。

“好的”,小菜回应道,随后小菜就在本子上迅速的记着。。。

五、缓存一致性问题

“好了,接下来,我们就来分析下你昨天代码出现的问题原因吧”。

“好的”。

“我们还是先来看看你昨天写的代码”,老王一边说,一边打开开发环境,打开了小菜昨天写的代码。

public class WrongCounter {

    private int visitCount;

    public void accessVisit(){
        visitCount++;
    }

    public int getVisitCount() {
        return visitCount;
    }
}

“现在我们就结合这个有问题的类来讲,假设同一时刻有两个线程调用了获取商品详情数据的接口,两个线程都触发了WrongCounter类中的accessVisit()方法。也就是说,两个线程都执行了visitCount++操作,你知道visitCount++操作在内存中是如何执行的吗?”,老王问小菜。

“这个不太清楚”。

“好的,那我们就来讲一下,其实visitCount++操作总体上会在内存中分为三个步骤”。

1.从主内存读取visitCount的值。

2.将visitCount的值进行加1操作。

3.将visitCount的值写回主内存。

“我们一步步讲,这样你也好理解些”,老王继续说,“我们先来看第1步:从主内存读取visitCount的值。假设同一时刻有两个线程同时调用了获取商品详情的接口,并且触发了 visitCount++操作,此时结合Java内存模型看的话,就像这张图一样”。

老王是真厉害,随手又画了一张图,如图2-4所示。

图片图片

“在步骤1从主内存读取visitCount的值时,线程1和线程2都会把主内存中的visitCount值读取到自己的工作内存中,此时线程1和线程2自身工作内存中的visitCount值都是0,这点能理解吗?”。

“能理解”,小菜回应道。

“好,我们再来接着讲第2步:将visitCount的值进行加1操作,还是画一张图看的清晰些”,老王又画了一张图,如图2-5所示。

图片图片

“此时,线程1和线程2都将读取到自己工作内存中的visitCount的值进行加1,此时线程1和线程2各自工作内存中的visitCount值都是1,这点能理解吗?”。

“能理解”。

“好,我们再来看第3步:将visitCount的值写回主内存,还是来一张图”,老王确实厉害,随手又画了一张图,如图2-6所示。

图片图片

“线程1和线程2都会将自己计算出的结果数据写到自身的工作内存,再刷新回主内存,在实际场景中,线程1和线程2的执行结果刷新回主内存的先后顺序是随机的,可能是线程1的结果先刷新回主内存,也可能是线程2的结果先刷新回主内存。但无论是先刷新线程1的结果,还是先刷新线程2的结果,最终主内存中的visitCount的值都是1,这与我们期望的结果不同,我们期望的结果是2,实际结果却是1,这下明白昨天你写的代码为什么出问题了吧?”。

“明白了,明白了”,小菜连忙回应道,”真没想到写个功能还牵扯到这么多知识点,真是又学到不少知识呀“。

六、如何解决问题

“现在明白了昨天写的代码为何会出现问题,那知道怎么解决吗?”,老王问。

“大概知道点,但是不是很明白”。

“好,那我们再讲讲怎么解决问题吧”。

“好的”。

“我们再次看看visitCount++操作在内存中的执行流程”。

1.从主内存读取visitCount的值。

2.将visitCount的值进行加1操作。

3.将visitCount的值写回主内存。

“既然visitCount++操作在内存中的执行流程会被分成3个步骤,那如果能够保证这3个步骤的原子性,也就是说,线程1完全执行完毕这三个步骤,线程2再从主内存中读取数据进行处理。或者线程2完全执行完毕这三个步骤,线程1再从主内存中读取数据进行处理,这样就能保证最终的结果数据与预期相符。这么说明白了吗?”。

“明白了”。

“好的,那我们今天就讲到这,根据今天我们讲的内容,你把昨天写的代码尝试修改下,不明白的地方再来问我”。

“好的,谢谢你,老大,今天确实学到不少知识,回去我也好好总结下”。

七、本章总结

本章,以故事场景的方式结合代码问题,以图文的形式重点介绍了CPU内存模型和Java内存模型,CPU内存模型和Java内存模型虽然平滑了CPU执行计算与读写主内存之间的速度差异,但是也引入了新的问题,那就是缓存一致性问题。本章,也结合代码示例与图文详细介绍了缓存一致性问题。最后,简单叙述了如何解决相关的问题。

最后,可以在评论区写下你学完本章节的收获,祝大家都能学有所成,我们一起搞定高并发设计模式。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论