00:00
也失效了,你得去内存里头重新再给我读一遍。所以我改了1亿次,你改了1亿次,互相通知来通知去,通知来通知去,都把时间消耗在互相通知的。这个消耗上了。不知道我说清楚没有。那有同学可能就会说了,说老师那第二个程序就跟这个不一样吗。来,我们来看一下第二个代码。第二个程序代码。这套程序代码什么样啊?你仔细看看。第二个程序代码是我在数据前面怼了七个long。那也就是说如果是。缓存行正好是。对齐前半截的话,那就是从P一开始,一直到这个X,这个八个位于同一行。那如果是往后来呢,就是这哥八个位于统一好。那如果是中间的,那无非就是说我这有几个,加上中间这个X,再加上后面几个位于同一行。
01:06
为什么是七个?小伙儿。64个字节为一行。一个long占八个字节。你要把剩下的占满。它不是七个浪吗?七个浪七个隆咚呛。好,没问题,没问题吧,哎。所以这种写法就产生了一个后果,这个后果是什么呢?就是我这X啊,你放心。无论你怎么排列,你在内存里排来排去,它绝对不可能和另外一个T里面的X位于同一行,这哥们绝对是单独的一行。单独一行有什么好处?单独一行的好处就在于。我这个只改我自己的这个X呀,你这个只改你的X呀,我们不用互相通知来通知去的,保持一致呀。
02:02
所以这个效率变高了。OK,这个小程序呢,我就给大家讲完了,不知道大家get到了没有。好,我们可以,我们可以继续吗?不太懂啊,似懂非懂。哦,怎么能这样子呢?这么简单的小程序,还似动非动啊。好那个。这个小程序,这个小程序还还似懂非懂啊,其实他们两个的区别吗?来我们我们稍微花一点点时间复习一下啊。这是第一种写法。好,这是第二种写法。
03:04
来,这是第一种写法,这是第二种写法。我们来看如果,如果,如果是第一种写法。这哥俩,这两个X。是在同一行里会发生什么?会发生是我CPU改完一个X之后,我得通知另外一个CPU说这行的数据已经失效了,你得重新再给我读一遍。然后如果是第二种写法呢,如果是第二种写法一定是这样的。这有,这有1X,这有一个X。然后这行里头只有这个,这行里头只有这个,互相之间互不打扰,我不用互相通知。好,还有没有同学有疑问呢?这个小同学们的基础稍弱啊,最后再重新过一小,如果是第一种写法,由于我们是两个T,所以两个X很可能位于同一行,那么两个X位于同一行的话呢,第一个线程在这个CPU里执行,第二个在线程在这个CPU里执行。
04:06
第一个线程修改完这个X,得通知另外一科CPU说,哥们儿,我这边失效了,你得去重新再读一遍。第二个线程修改完这个X,得通知第一个CPU说,哥们,我这边失效了,你这个行得重新读一遍,所以互相之间为了保持一致性,把这一个资源都耗费在这儿了。如果是采用这种写法呢?这种写法是无论你怎么排列,你放心。第一个X只是位于这一行,第二个X位于这一行,那么第一个CPU呢,就不只要改这一行的数据就可以了,第二个CPU只要改这一行的数据,他们互相之间不冲突,我不用通知另外一个CPU说这行数据失效了,为什么?因为另外一个CPU不拥有这行数据。为啥是两个X?你怎么还会问这个问题?你是怎么想到这个问题的?我实在是相当的佩服。为啥是两个X,因为代码你写了呗。
05:02
代码里写了A0 a11个数组不就两个X吗?嗯,好,我们可以继续了吗?可以继续给老师扣一来。好,我们继续。呃,那有同学可能会说啊,说那个老师真的有人这么来写程序吗?呃,如果你真的读过那个JDK1.7版本的。Concurrent hash map,著名的道里。这哥们儿就是这么写的,好吧?呃,如果你了解过有一个著名的框架,这个框架呢,叫disruptor,有没有同学了解过?This Roger。把它翻译过来呢,叫做闪电,它号称是单机版最快的MQ,听我说它的核心是叫做一个环形缓冲区。
06:09
叫环形缓冲区,别人家的缓冲区呢,都是一个数组头指针尾指针。在这里面呢,转来转去,缓冲区的数据呢,往里装往往外读,但是这哥们是一个环形缓冲区,环形缓冲区的意思是你看啊,这里有两个指针,但是环形的只需要一个指针。一个指针指在这儿,等到结尾的时候呢,又从又开始好一个环形的缓冲区,如果你学学过red,其实这个也大概也能想得起来是吧,缓存一致性协议,OK。看这里这个环环环形缓冲区,它有一个指针。这个指针。很可能在多线程的情况下问为什么,你想想看,有好多人往里扔,有好多的那个,呃,消费者往外拿。那么为了提升它的效率,它是怎么写的呢?你来读一下啊,看看能不能找不着。
07:01
好了,这是它的环境缓冲区,这个缓冲区呢,叫做ring buffer r环状的buffer缓冲区ring buffer。点进去。好哥们儿,你看看。在这个缓冲,缓冲区里面有一个很重要的变量,这个变量呢叫做initial cur value,就是我们那个指针。游标。看到后面。What?怼了P1到P7。是吧?嗯。有同学说前面有吗?注意看他是从谁继承的呢?从buffer fields继承下来的,所以我们看看他的父亲。Uff pad OK。这是他的父亲,你会发现他父亲前面有七个,后面有七个,所以你无论怎么排列,这个指针绝对不可能位于同一行里面。这也就造成了它的效率非常非踌。
08:00
呃,这个框架呢,是得过哪个奖,哪个奖来的世界的大奖。它是单机版最快最快最快的MQ,所以如果你想执行我们单机的这个效率的话。想让单机的效率做到最高的话,你可以考虑使用闪电。呃,黄老师打算在我们的P8的架构师课程里面那个再到淘宝的这个项目里来,使用R来处理交易,这个等他们那边代码出来可以,大家可以拿去用。好嘞。到现在为止,大家是不是知道了啊,缓存行确确实实是存在这个概念的。那有同学会说了,老师这个互相之间的缓存保持一致性这件事儿。它叫做什么内容呢?听我说这东西呢,叫做缓存一致性协议,我相信呢,很多很多同学啊,都听说过这个概念,就是缓存一致性协议。
09:02
然后呢,好多人还会听说过这个词,叫me SI,听过同学老师扣个一来我认识一下。应该听过。好多,但是好多人啊,会把这个mesi等同于。他就是一个什么,呃,缓存一致性协议大哥,这事不对,认真听啊,这个事儿不对。缓存一致性协议只是一种通用的叫法,Me SI只是是英特尔英特尔芯片上英特尔的芯片。它的缓存一致性协议的实现,好这个英特尔在英特尔的芯片上,然后这个东西叫me SI。但是在其他的这种处理器的架构上,它未必叫me SI。它叫做什么呢?他可能叫MSI,叫mosi,叫S,叫fire fly,叫dragon啊,这些都是缓存一致性协议,所以你千万不要跟面试官讲缓存一致性协议就是mesi,不是。
10:03
其实如果你想了解整个缓存一致性协议的一些内容的话呢,大概大家可以去百度上查一下,或者我给大家给出来的这个链接,你自己去找就行,MSI本身的理解呢,并不是很难听,我简单跟你讲几句。这个me SI的全称其实就是几个单词的缩写,几个单词的首字母缩写。这几个单词分别叫做modified,分别叫做exclusive shared和invalid。Modified修改过了exclusive独占share的共享inval的失效。这里呢,指的是缓存行的状态,假如有一行位于这颗CPU,它改过了之后,这一行也位于另外一颗CPU,那么这一行。在这个CPU里面就叫做modified,被修改过了。与此同时,通知另外一个CPU,告诉你这行的状态修改为invalid失效了。
11:02
既然你是失效了,你是不是得去重新去内存里头读啊,你再读一遍,读过来的就是最新的数据了。好,Exclusive叫独占,比方说我们第二种写法,你放心,它只是独占一行,没有其他任何的CPU和他共享,那这个时候你随便改share的共享,共享的意思是我这边也也可以读,另外一个CPU也可以读。这个呢,就是缓存一致性的协议的一个简简简简单的说法啊,当然他协议本身比较简单,但这个是牵扯到硬件级了,我个人建议呢,不要在这上面花太多时间,一般面试官问到这儿也就差不多了。好,到现在为止,我相信我已经讲讲清楚缓存行这个概念,还有缓存行啊,缓存一致性协议这个概念了。来可以继续。同学老师扣一。那有同学可能会在这里问我一个问题啊,说老师这个东西跟volatile有半毛钱关系吗?其实严格来讲啊,这个东西跟volatile关系并不是特别大,起码跟Java里面的VO关系不是特别大。
12:04
呃,这个呢,只是硬件层级,两颗CPU之间。做数据一致性的一种实现协议。有一些,还有各种各样的实现协议。我给你举个最简单例子,有有的有的数据特别大个。他超跨越了一行。他一行都装不下,那你说怎么做一致性啊?用缓存行缓存一定协议就不行了,这时候得用别的协议才行,我告诉你一会我告诉你好吧,不着急,大家明白这个缓存行的基本的概念之后呢,我们下面来探讨另外一个概念。嗯。好,看这里。嗯,我刚刚呢,讲完了前三个问题啊,我们来讲这个后面几个问题。
13:06
呃,我们来讲VO这个概念。大家知道那个。呃,Java里面呢。有一个关键词叫VO是吧,嗯。这个是那个。编码的问题啊,我先不管他了。好,同学们大家看这个小程序啊,下面呢,我来呃正式的开始讲这个VO这个概念,呃,VO这个概念呢,它。我为什么上来先给大家讲这个缓存一致性协议呢?主要是原因是,呃,网上有N多N多的文章把volatile跟那个缓存一致性协议混在一起讲。就说VO啊,底层是通过缓存一致性协议实现的,我跟你讲真心不是,绝对不是,一会我给你讲到源码你就明白了。
14:06
真的不是那么回事儿。所以我先给大家解释了什么叫做缓存一致性协议,大家听我说啊,这个缓存一致性协议呢,不管你你用不用,用不用VO,它这个一致性该保持一致还是要保持一致,它这个同步的时间也并不是能够100%的确定在什么时间同步,嗯,你得是你的程序来控制才可以。V呢,是完全可以控制你的两个线程之间的可见性,有两个CPU之间数据的可见性的,但是是不是一定是用缓存一致性协议来实现,真心不是。在这呢,理理解了这个缓存证协议的基本概念之后,我们来聊water会更好聊一些。网上的文章很多很多都是不对的,你记住这一点,记着结论就行了啊。看这里。作为VO来说。它有两大作用,这是面试题里头经常被问到的啊,VO也是一个面试的重灾区,虽然我们你们平时写程序真真的不怎么用。
15:06
很少有人用VO。呃,你你用的类库里头可能人家用到了啊,你知道有这么回事儿,呃,你你你写一个锁的时候,那个aqs里面人人家可能用到了啊,你知道有这回事儿,但是你自己写东西,你很少用这玩意儿。但是面试这个是一个重灾区。刚才有同学就一直问我说,老师这东西我实际当中用不着,为什么要学习它?你学了,他面试通过50万,你不学,面试通过25万,你爱学不学。面试造火箭,入职拧螺丝,你先把造火箭的技术学了,再慢慢去拧螺丝。就差这么多,没错,大家看这里,作为这个来说,它有两大作用。窝头两大作用。第一大作用就保持线程的可见性。线程间可见。
16:02
第二个叫禁止指令重排序。禁止重来。好,我们一点点来讲,昨天呢,我要求大家做一个小小的预习,至少你要了解volatile的线程,可见是什么意思。避免有同学没预习,我做个小小的简单的解释,大家来看这小程序,这小程序嗯,非常简单,在我一个class里面有一个成员变量,这个成变量呢,是布尔类型的running等于true。那么。M方法里面。I'm start。打,呃,打印出来,M开始,然后while running,只要是这个running等于出的话,这是一个死循环。那么他一定不会结束。所以当我有一个线程,这里有一个线程,你有一个thread,执行的是t.M的那个那个那个方法。他的名字呢,是T1,然后让他start之后,大家可以想象一下,这哥们会不停,停不下来,一定是停不下来的。
17:10
好,我想让它停下来怎么办呀?想当然的方式就是把这个running设为false,你就应该停下来了呀,所以我在主线程里面睡了一秒钟之后。直接把这个running设成了。那么我想问大家一句。程序能不能结束?可以吗?来跑一下看看。Run。大家看到了M42,无论你等多少秒,程序都不结束。哎,为什么不结束啊?不结束,其实跟我们刚刚讲的缓存一致性的这个协议。有,呃,异曲同工之妙,大家得想象一下,这个running是位于我们的主存里面。它的值是true。
18:02
那么当我一个线程去读取里面值的时候,While running。要读这个值,读到我的线程本地,就是CPU里面的寄存器里面。读到我的线程本地,然后我每次循环注意只会看这个线程本地的内容,我不会去内存里头重新再看一下的,不会的,因为没有人通知我要去看。所以。这个值永远是除,即便是有另外一个线程把你改成了false。但是对我来说,我看不到。那有同学可能会说,老师,我怎么才能看到呢?看到很简单,忘了它的作用就在这儿。如果在前面加条。跑一下。I'star。这是我小程序的问题,我这个。Idea的版本有问题,正在重装,我重新把它编译一下才可以。
19:02
好一下。I'start你看,一秒钟之后I'm就结束了。为什么会这样?好,这是VO的第一个作用,修饰的任何一块内存。Wall修饰的一块内存,这块内存是true。你对里面做的任何改变,其他线程马上可见。这就是可见性,保持线程间的可见性。OK,这是VO的第一个作用。来能get到这一点。同学老师扣个一。来看这里。好,再看这里。我知道有同学呢,老有这样的疑问,说老师,假如我在。我不用多条,那么你应该是。
20:02
结束不掉的。假如我的里。我写一个输出语句,Hello,不断的输出,不断的输出,那么它会不会结束掉呢?那他就开始做实验,重新编译。跑一下。他发现结束了。他会说,老师,你讲的不对啊,为什么这就结束了,听我说。在执行语句的时候,有很多很多的语句,它中间会有一个和内存同步的过程,Systemlo在这个语句执行的内部,这个方法执行内部,它会内存和我们的本地缓存做同步。听懂了吗?所以如果你是打印语句,很可能就结束了,但是这是你的运气好,如果你里边写的不是打印语句,就不一定了,叫做不一定结束。好吧。
21:03
好了,我们可以继续了吗?大家应该能明白volatile的这种。现成可见性了是吧。当然线程可见性呢,本身非常的简单。呃,其实比较难的是另外一个就是。哪个内容呢,是。叫做禁止指令的重排序啊,这里头有一个特别牛逼的问题,呃,每次讲这道题呢,讲的都挺累的,但是没办法给大家讲讲,讲讲看吧,好,听我说啊。这是美团的一道面试题,这道面试题答出来真的是50万以上。这样面试题是这样的,叫做DC单立。要不要加VO?What?这里面题很很很很,同学,有同学就听懵了,老师,什么叫单利我不知道,什么叫DC我也不知道,罗是什么东东我也不知道。
22:04
OK,为了讲清楚这道题。所有同学都支起耳朵来好好听。讲完这道题,基本上就差不多了。好。看这里。美团上来不会直接问你啊,会让你写单利模式,字要写的好看点。有点对,你知道字为什么不能写好看吗?原因其实非常简单。因为我用的那个画笔啊,它不好用,我如果在这儿写字,会写的非常的好看。比如说这个老马家的人。嗯,重新写一下,开始写字。
23:00
书写笔。除夕。怎么竟然还用的是你啊,开个玩笑来。脑壳疼是吧,嗯。大家觉得这个字还可以吗?鸡皮疙瘩都起来了是吧,嗯。写字这件事呢,老师还还是可以的啊,老师曾经也是练过书法的人,开玩笑。嗯。因为真正写的话呢,那个那种在那个哪儿。
24:06
好,我现在是在讲书法课吗?没有吧?开玩笑,不讲书法了啊呃,这个没办法啊,因为用用用这个东西在这画的话呢,控制控制不好笔触啊,人没招,嗯,开始干奇怪的事情,好吧,好好。大家放松一下啊,没有别的意思。来,我们继续啊。呃,为了讲清楚这道问题啊,这个有点费劲,我呢。为了讲清楚的问题,我需要给大家补东西比较多。
25:01
我需要给大家补点很多的东东了。嗯。好,看这里。呃,我要下面要给大家讲的这个问题呢,就是这这个问题啊,就是美团一般是问你,呃这么一句话,它的创建过程啥样,你答出来了之后,问你TCLR要不要加VO。你就很难了。我给大家讲讲这个问题啊,好好听,认真听,洗洗干净耳朵。嗯,要讲清楚这道问题呢,需要给大家打的基础比较多,我一点点来打,我们首先来聊第一件事儿就是什么叫做。指令的乱序执行指令的乱序执行是一个什么概念?
26:03
OK,看这里。呃,作为一颗CPU来讲,由于它的速度非常快。那么。你未给他两条指令,指令一,指令二,你未给这颗CPU2条指令。那么在真正执行的过程中,很有可能并不是指令一先执行,指令二再执行。很有可能是执行指令一的过程之中,指令二就已经在执行了,甚至指令二已经执行完了,指令一还没有执行完。举个最简单例子,如果你是烧烧壶水,你先洗水壶,烧开水洗茶壶,洗茶杯,拿茶叶泡茶。那么在CPU的真正执行过程中,很可能是这样的,洗水壶烧开水的过程,他就开始洗茶壶、洗茶杯,最后拿茶叶泡茶。所以通过这样的一个小例子,你会发现,如果CPU能够。乱序执行。所谓的乱序其实也不是乱序,就是把后面的一些指令提到前面来,执行的时候很有可能会提高效率,这是CPU为什么支持乱序执行的理论基础。
27:12
那么我们讲的专业一点,假如有两条指令,第一条指令是去内存里头读一个数据,刚才我说过,CPU比内存的速度大概要快100倍,如果他发出一条指令,一个时钟周期,E机就去内存里读数据了,但是等数据回来,他得需要干干的等待99个时钟周期。然后才能执行第二个。大哥,你稍微想象一下。那个。你这样的话是不是浪费CPU的时间了。CPU效率就不高了,所以在CPU进行优化的时候,有可能是这样来执行的,我第一条指令区内存读数据,在我等待返回的这个期间,我把第二个指令拿到前面来执行。当然这里面有前提,就是两条指令不能存在前后的依赖关系,你比如说你第一条,第一条指令叫I等于八,第二条指令叫爱加加,你绝对不可能把爱加加跑到前面来,不行,这为什么?这个是前后存在依赖关系的,这这这个不行。
28:13
好,这是CPU乱序执行的最基本的概念,这个概念听清的同学老师扣一,我们继续。嗯。那有同学可能会说,老师,这个能证明吗?这个可以。我给你证明一下。这个是我在另外一个项目里头证明的。这证明起来有点麻烦啊,其实你知道有这么回事也行,不给你证明也可以,这个听你们的啊。证明起来稍微有点麻烦。这是一个经典的证明,超级经典。呃,先说这小程序不是我写的,也不是我设计的,这小程序最早是以老外设计的,拿C语言写的,后来国内的人把它改成Java,然后呢,我拿过来之后讲给大家听。
29:07
这不是我的劳动成果啊,也向这位写程序的人致敬。这个特别好玩。证明一件事情存在乱序执行,其实很难很难证明,但是呢,它设计了一个特别精巧的小程序来证明这一点。呃,理解这个小程序,需要大家有一点点小小的业务逻辑。这个业务逻辑呢,也比较简单,你认真听我讲就行了,好,大家看这里。我们下面来证明乱序这件事情是存在的这个小程序啊,记住,这个小程序的使命叫做证明乱序存在。好吧。小程序怎么证明呢?这么来证明。现在呢,我们有四个值。第一个值是X,第二个值是Y,第三个值是A,第四个值是B。然后不停的死循环,一直不停的起两个线程。
30:04
这两个线程,第一个线程干了一件事,叫A等1X等。
我来说两句