00:00
同学们,接下来给大家介绍一下GMM规范的三大特性,哪三个呢?分别是可见原子和有序。那通过前面的讲解我们明白GMM内存模型它不是实物,它是一种抽象的规范理念和概念,主要用于屏蔽各个操作系统之间。对于多线程访问或者是CPU跟内存之间访问数据的读取和共享,以及可见屏蔽这些底层硬件的差异,那么接下来我们呢,在打开脑图之前,我们先来简单的说一下什么叫可见原子和有序。首先。我们前面强调过,在高并发多线程的环境下面,内存条上的数据多个线程假设来读取,来访问某个线程给他改了,它需要把它的。修改结果刷新回主物理内存,那么他怎么让大家知道现在内存里面的数据已经变了。怎么让大家。收到这个通知呢,可见来什么叫原子,前面讲过snchizelo unlo等等等等,说过很多遍都清楚。为了避免。
01:06
多个线程对同一份资源的增强太过于激烈,导致数据一致性受到破坏,那么我们是不是规定同一时间段只能有一个线程来写操作,保证他写完了?他提交了数据稳定了以后,咱们再公布公开。所以这些数据的修改,一有唯一排他性,二要么一起成功,要么一起失败,这个呢就是原子,那么三什么叫有序呢?那么首先。不是像你想象的。好,我的程序写完了,从上到下,12345,我怎么写的,你该怎么执行,而这个有序性牵扯到所谓的指令重排序,它的排序。是什么样的?也记你写程序的时候可能是12345,我执行的时候可能是345,一二都有种可能,那么这个有序是指指令重排序的意思。OK,好,那么粗浅的入门讲解先带大家混个耳熟,待会儿我们逐个展开,那么你听完这个以后你就会明白。
02:09
这么牛逼的一套规范,三个立足点就给你搞定,深刻体会什么叫高端的食材,往往只需要最朴素的烹饪手法,那么牛逼的技术往往也只需要最简单的真理性描述,正所谓大道至简。好,那么同学们下面请看一下可见。来。是指当一个线程修改了某一个什么共享变量的值啊?其他线程是否能够立刻知道该变更?哎,我们前面强调过100个线程来协作其中有一个。把主内存里面的一个变量值给改了,其他能不能知道,所以说这种就叫可见性注意。GM规定了所有的变量都存储在什么主内存当中,好,那说人话,假如说啊,我现在有一个class。
03:03
比方说我这有个dog。那么这个多呢?In特age OK,假设这是一个对象,我六出来一个对象,在Java虚拟机的堆里面设定了一个东西,就假设这只小狗它是三岁,那么现在有个A等于三,那么好,各位同学,对于这这个对象age等于三,它就叫对象的共享变量,它存在什么地方啊主。内存一定要注意是主内存,那么左边这个是硬件,前面说过了一台电脑。RAM就是我们的主物理内存,这是两核,两个CPU,当然你是四核或者是八核,再说那么寄存器在CPU和主物理内存之间有三级缓存,也就是我们CPU cash memory,那么接下来这个是硬件抽象出来GM memory是这样的。我们刚才。六出来的那个小狗对象就在我们的内存里面,这有个共享变量int age等于五,我们晓得的,假设有两个线程来操作这个对象,可不可以就像我们多个线程卖票一样,点一下这个票就少一张?假设我们点一下给这个狗的年龄就加一岁,完全可以吧,但是票数或者这个年纪就是一个共享变量,在主内存里面有且仅有一份。
04:20
OK,那么下面。GMM规范的可见性是这样的,它有两条,请大家务必要了解。第一个,如果这个A等于五岁。A线程像把它加成六岁,它不可以直接操作主内存。那么他们的按照GM的规范是这样的,每一个线程它先去读。主内存共享变量的值,比方说现在年纪啊,小狗年纪A级等于五,二读回到自己线程A里面所持有的本地内存里面,俗称共享变量的副本,我这儿考A等于五,我这儿考个A等于五,你们两个各自在各自的D盘上去修改,也即因为你中间有可能你改错了,你不能说改错了以后我再涂,把它涂抹了,大哥,这是主内存是大家共用的。
05:12
你要改只能从我这儿读取,拉到你自己本地,你在本地修改了以后再给我提交回来,OK,所以说这个就是GMM所谓的可见性前提下,对。线程对主内存读取共享变量的一个流程,那么一读取,二读到自己线程的本地线程内存里面,形成共享变量的副本,自己修改完了以后再写回去。主内存。那么此时相当于说。A,改完了,我把它从五改成了六,那么我要让其他线程马上知道可以到主内存当中读取最新的数据,这种东东就叫什么。可见性,这是它第一个特性,好,那么接下来我们来看一下。
06:01
首先。为什么要设置这个呢?它的作用和目的。系统主内存的共享变量的数据修改被写入的时机是不确定的,我并不知道下一秒钟是A是B是C是D是F哪个线程来访问我的主内存里面的共享变量,多线程的并发下就很可能出现暂读,也就是说我读的可能不是最新的了,别的线程早就已经把它修改了。所以。为了避免这种情况,尽量的我们抽取出一种规范和邀约。每个线程都有自己的什么工作内存线程自己私有的线程,自己的工作内存保存了该线程使用到变量的什么主内存副本拷贝,刚才是不是说过你要想去修改主内存中的东西?给小狗的年龄加一岁。读过了,在自己这儿改完第二步、第三步再写回去。所以说线程对变量的所有操作,读或者是写、复制等,都必须在线程自己的什么工作内存中进行,而不能够直接读写主内存中的变量。
07:07
线程你只能操作自己的,你不能去操作主内存,你只能去读到本地,在本地自己的私有域里面去修改,不可以操作主内存,说白了你可以。不吃自己这碗饭,但是你不能砸这口锅。好,假设这口锅里面。熬了一锅粥,有人喜欢吃吃甜的,有人喜欢吃咸的,对吧,有人喜欢吃辣的,那你不能说我喜欢吃甜的,我就往这个共享的这口锅里面,把这个粥变成了一锅甜粥,那这个冰县城是不是就怒了,所以说大家只能是什么原味白粥在这个大锅里面谁爱去吃啊?那么请在自己这儿去修改,OK,是这个意思,好,那么下面。不同的线程之间无法直接访问对方工作中的变量,一句话,本线程的共享变量这个地域是私有的,只能自己操作自己,我不可能横向A,不可能操作B,所以说如果我想把我的修改通知给B,让B使用,只能通过主内存来做一个交换,先由A给主内存,再让B读取,中间就靠GM可见性这个规范来控制。那么所以说线程之间无法直接访问对方工作中的变量。
08:26
他们之间的传递均需要通过主内存来完成,中间不可以砍掉中间商,只能有主内存来协调好。那么所以说线程、主内存、工作内存,它们三者的交互关系就这样,一个是读取模式,跟操作系统和硬件无关,都是靠这一套线程。把主内存的变量读到自己的工作内存里面,修改完了以后再写回去,好,那么接下来我们来说一下在这种模型下面会产生的问题,这也就是什么好,现在倒是可见改了,我可以通知你,问题是。
09:03
如果你不加原子性,那么会产生一种什么线程,什么单读?好,我们来吧,什么叫线程单读呢?同学们请看,主内存中有变量X初始值为零,那么假如说这有个主内存初始变量就是零,或者A级小狗年级等于五都一样。那么线程A要加X加个一,那么先加X等于零主内存这个值拷贝回自己的私有内存中,然后更新X的值,它是零。第二步A拷回到我这儿做,把我们共享变量副本的X等于X加个一,那么零加一这个A里面的就是一,此时第二步完成以后第三步。线程A准备将更新后的X等于一的这个值刷回到主内存的时间是不固定的,也就是说可能我刚刚算完CPU的调度上下面的切换我就停了,因为线程的启动嘛,对吧,而此时的时候我可能就在这儿。
10:03
挂起了,突然多线程B或者其他线程来了。注意,A是希望把这个值从零改为一,再刷回去,但是A现在做到一半半调子工程停这了,A里面是X等于一了,但是B呢,去主内存的时候,由于A还没有把自己的劳动成果写回来。导致B去主内存图的时候是几还是?是吗?零,所以说刚好在A线程没有回刷X到主内存的时候,B同样从主内存读取X,此时主内存的值还是多少?零?那么和线程A一样的操作,最后我们呢,就会变成这样一种情况。不去读啊。是零,B也加个一又写回去,这是一,实际而言,我们的流程,我们的业务是希望什么?A马上刷新回去,让主内存的值是一,B再去读的时候,主内存的值是一,它再加个一,再写回来,变成了二,那么这样就会导致我们的是吗?最后期盼的X等于二就会变成X等于一,咱们是不是写丢了一次啊?
11:10
哎,所以说这个就是我们的什么可见性非常的重要,换句话说,为了解决这种问题,我们就要保证。改的时候只有一个线程来改,不要错乱,要保证原则,改完以后要立刻通知别人,哎,兄弟主内存的值已经被我更新了,你手头上那份给我作废去拿最新的,那么所以说这样情况下才能避免这些单毒,那么可见性是一种及时可见,及时通知机制,那么大家务必引起重视,好所以说这个就是我们第一个特性,那么所谓原子是指同一个操作不可打断,多线程环境下面操作不能被其他线程干扰加锁,好吧,这个呢,我们就不再赘述,那么接下来。我们呢?来说一下所谓的有序性是什么意思啊?牵扯到一个概念叫指令重排序。那么不妨。
12:00
我们先简单的先过个案例啊,那么来先说案例,再说理论,再回来再说这个案例,下面同学们,我呢。给大家弄的情况是这样的,假设我们这儿啊,一个源码里面写的是一句1234啊。X等于11 Y等于12X等于X加5Y等于X乘以X。我们都知道,我们程序有个语法要求。先定义后使用对吧?那接下来大家请看我的问题是假设我们现在写了以后。底层会给我们重排序啊,把我们的执行顺序。打乱,那么请问现在这个顺序语句12341234这个顺序行不行?2134这个顺序行不行?2134就是Y放在第一行前面,这个程序的效果一不一样。第三组1324。一。三。
13:00
二。四请问这个顺序行不行?那么下面最后一句四。可不可以拉到第一条叫4123。可不可以好。那么同学们,我们。可以是为什么?不可以是又为什么?那么我们带着问题先看一下我们的有序,也即指令重排。下面跟着来。第三个特性。什么叫有序?是指。对于一个线程的执行代码而言啊,我们呢,一般都会认为我怎么写的对吧,比如说我们经常写这是个main方法,从上写到下,我怎么写的程序怎么执行,实际而言,底层还真不一定是这样,尤其在高并发多线程的环境,你是一个只有一个线程内线程单线程的时候,哎,几乎可以这么理解,但只要牵扯到多线程高并发这个事儿就复杂了。那么下面请听我来。有时候呢,它底层为了提升性能,编译器和处理器通常会对指定序列进行重新排序,那么Java规范规定GM线程内部维持顺序化,意义也就是尽量是从上到下,但只即只要程序的基最终结果与它顺序化执行结果相等,那么指定的执行顺序可以与代码顺序怎么着不一致啊,此过程就叫。
14:22
指令重排序,也就是说它的第三个特性有序,有序是指指令重排序的静止,或者说是支持,或者说是判断,那么它的优缺点是这样,为什么会产生这种东西呢?GM呢,能根据处理器的特性啊,我们前面强调过,因为这个机器有苹果的,有Linux系统,有Windows的,为了保证利益最大化,性能最优,适当的对机器指令进行什么重排,使机器指令更能符合CPU的什么。执行特性,最大限度的发挥机器的性能,这是优点,但是指令重排可以保证什么创新语义的一致啊。
15:02
但没有义务保障多线程间的语义一致啊,也就是说还是会产生单独,就是在高并发下面这种指令重排一旦发生了,后面我们会说啊,它有可能导致单独,所以两行以上不相干的代码在执行的时候有可能先执行的是吧,不是第一条,哎,不见得是从上到下的顺序执行,执行顺序会被优化,那么就要看这个优化效果和我们的期望是不是一样,如果一样皆大欢喜,如果不一样,有时候我们会。用。一种方法禁止指令重排序好。那么下面。从源码到最终呢,我们要清楚我们写好的,比方说hello word.java啊,假如这么一个,它从你写完编辑执行以后到最终执行指令,中间要经过三段,分别是编译器的优化,第二个是指令并行的优化,第三一个是内存系统的优化。那么在这。听好,单线程环境里面一般不会出事,不用多谈,然后假设就是你现在是尤其仅有一个县程对吧?山中老虎猴子称带碗,你爱怎么嗨怎么嗨,但是问题是处理器在进行除排序时候干嘛?它必须要考虑指令间的什么数据依赖性。
16:16
说人话的意思就是比如说。我先出来了,一定是先由我爸爸先出来,它有一个逻辑先后关系,在你可以为了提升性能,你可以重排,但是重排的前提并不能违背数据依赖性好。那么最终我们会发现在多线程环境中的线程交替执行,由于编译器优化重排的这个存在,这个不是以你的意志为转移的,这是底层操作系统所规定的。那么两个线程中使用变量能否保持一致性是无法确定的,结果无法预测。所以有时候我们要根据特殊场景的业务禁止指令重排而保证程序执行的是吧,有序性。哎,那么这儿大家请看。
17:00
根据我们这儿所说的数据依赖性的原则,我们再回到我们的案例来看一眼。大家看一下现在语句四。可以排到第一行吗?那么这种情况下就绝对不可以,为什么?因为有一个东西我们这儿说过非常重要,数据的什么依赖性,大家请看我们的第四行是什么?Y等于X乘以X,如果它在第一行。大哥你根本就没有定义呢,我怎么使用。所以说一定要先定下才能使用,你要做什么事,逻辑上要符合数据依赖性,我Y和X要使用,是不是要先依赖?你要先在我使用之前先定义,你定义都没有定义,我怎么知道X到底是个什么鬼,所以说这个时候程序就会报错,这种就不允许指令重排,而其他情况下1234。2134,比如说我这个程序要。执行和运行先定义X还是先定义Y,这个无所谓,这种你在底层排序,只要利于你结果运算的优化,这个我是允许的,这个就不用禁止啊,但是对于四种情况是绝对不允许的,这个就要禁止指令重排,OK,那么一句话,由于各个操作系统它不一样,虽然说。
18:15
我允许指令重排,但是呢,我们不在某些情况下要禁止指令重排,这个后面我们讲tell时候会跟大家详说,那么这个指令重排它达到的效果就是为了程序的性能最佳,比方有些时候啊,一加二加上三,就等于三加上二加上一,回答我顺序是不是被打乱了,比如说在苹果系统,它就语法规定是一加二加三。但是在0X系统,它的语法规定是三加二加一,但是不管怎么着,你最终结果是一样的,只要不影响最终的结果,OK来,只要程序的最终结果与它顺序化执行的结果相谈。那么指令的执行顺序可以代码的顺序怎么着啊,不一致啊,这种过程就叫指令的重排序,哎,这个就是我们GMM的三大规范。
我来说两句