00:00
接下来我们介绍V莱尔的第二个特性,没有原子性,也即被V莱尔修饰的变量,在高并发多线程的访问下面,它是没有办法保证数据一致性和安全性的,一定会出错。那么大家都清楚,一般我们在多线程的环境下面对某个变量进行访问,就像我们卖票的程序,为了保证原子性和数据的一致性,一般我们要加lock unlo或者是。SNCH等关键字,辅以相关的手段,如果没有他们,你仅仅是依赖一个vla想保证原子性,那么vla是不支持的,尤其啊,对于我们类似于这样的什么符合操作,比如说number加加,爱加加等等,V搞不定,所以呢它没有原子性。那么接下来。我们先用代码案例证明,然后呢再说底层原理,来,同学们。我们第二个开始。的非原子性案例,那对于我们这有个资源number OK,那么public,那么假设啊,我们大家都清楚,我们先用老版本的SYNCH的,那么。
01:06
At plus plus,类似于这样的number加加,那么大家都清楚。如果你要保证。它的原子性,那么你一定要是加SNCH关键字对吧,现在呢,我们先不用修饰,我们就是最中规中矩的,我相信这个代码呢,大家呢,一看就懂,那么好,老规矩。这个呢是多线程准备操作这个资源类,这个资源类里面封装了一个plus plus这么一个方法,那接下来。我们呢,有十个线程。这十个线程,每一个线程都要调用这个资源类里面的plus plus方法假设,嗯,1000次,好吧,那么来同学们。那么现在呢?我们大家都清楚十乘1000答案是多少,不用我多废话,那么下面来同学们plus plus没问题吧,那么相当于说呢,在上面呢,就有十个线程,每个线程要操作1000次,那么最后的值啊,应该是1万吧,好,那么这样我们用用个最简单的两秒钟给你算完,算完了以后,那么我们的。
02:12
线程,这十个线程完了以后,还有一个线程是没线程,那么接下来呢,我们来看看。我们呢,没线程来得到最后这个值应该是多少,好,大家请看啊,现在我们是加了S的,那么这个时候我们呢,运行一下这个程序,等待后台出现结果,大家请看,如果不出什么意外,只要是什么。Think这样的关键字修饰的,一般在多线程的环境下面,肯定是能够得到一个我们最终的结果和效果的,对吧,那么现在呢,我们呢,跑了三次以后,这个呢,大家呢是非常清楚的,好,那接下来。我们没有加深了。干掉那。换一下。刚才呢,不管跑多少次啊,我们的标准答案1万是没问题的,那么现在呢,我们呢,来跑一下我们的程序。
03:04
此时呢,对于修饰的number,我们进行number加加,这样的操作大家请看。是不是不见得?每一次啊,都能保证我们的效果,而且大概率来讲的话,都没有办法得到我们想要的正确的结果,OK,所以说实际案例我们可以获得volatile是不具备原子性的,你要有原子性必须要加lo unlo或者thinknch vla不具备好。那么接下来。代码验证完成以后,我们呢,回答我们的理论和面试如何跟面试官来进行回答他会问你为什么没有原子性好,那么首先啊。通过前面啊,还记不记得我们前面讲过一个变量的读取是不是。八步。那我们。读取一个普通的变量,就算它排完了,它要修饰。我们的读取规则是这样的。
04:00
假设你不加锁,不加锁就没有保证原子性,那么也就是说这个是内存里面的主内存对象同一范多个线程共享的,那么接下来我们呢,两个线程都要从主内存对象里面去读取这份共有的变量,读到自己的私域空间,自己的工作本地内存里面,那么也就是说按照我们前面所写的这些步骤。没加锁,你第一个线程发起的这一套操作时间里面,那么第二个线程随时可能对于这个主内存对象发起第二套操作,假设没有锁的控制,没有原子性的保证,主内存一个对象,比如说number等于五,线程一现在读进去,假设漏读到这一步的时候,线程二马上也可以去读,我们没有被控制,没有被锁死,各忙各的,那么这个时候。由于我们现在进行的是什么,就以我们的案例为为例啊,最经典的我们做的是number加加,表面上看在Java源代码这个级别,它只是做了一步操作,实际上而言,它呢是底层呢是做了三个操作,打开看一下。
05:05
那么大家请看主内存当中有一个共享变量count,类似于我们刚才源代码所写的。Number,那么前面我们强调过,在源代码Java这个级别,这只有一行,其实质而言,在线程工作内存加载分解了以后,是不是变成了三步?那么我们所写的I加加或者number加加这三步是非原子操作,没有加锁,底子被改成了加载、计算、赋值共三步,这就特别容易出现。原来变量修饰的常见的。符合操作的非原子安全问题,那么它的原理是这样的。Do you。关键字修饰的变量,它是具备可见性的,那么这个所谓的可见性也就只是保证从主内存加载到线程工作内存的这个值啊,是最新的,哎,也就是说只要有任何一个线程现在修改了这个V变量,其他线程马上可以具备可见性获得最新通知,就是主内存里面的值啊,已经被改了,那么也就是说它仅仅是保证数据加载的时候加载进来读取是最新的,比如说主内存里面现在这个值是个五,那么保证所有线程读到的最新版本都是我,但是呢?
06:19
对于写操作,在多线程环境下面,我们这个数据的这加杂啊,计算啊,赋值啊,是可能会出现多次的,那么大家请看啊,我们这十个线程,每个线程操作1000次啊,那么这样是不是肯定是出现多次啊,那么如果在数据加载之后,主内存当中V修饰的变量被其他线程所改了。那么现成工作内存的操作将会作废,对吧?我们都晓得的变量是具备可见性的,如果有人把这个里面的改了,你们马上。收到及时反馈的通知啊,哥们手头上这份不香了,咱们要去主内存当中去读最新的,那么会操作出现写丢失的问题,也就是说你这三步由于是没有加锁,你在这正在做着计算的时候。
07:05
你这次计算已经被其他线程赶在你前面已经提交了,你这次计算失效了,所以说各个线程私有内存和主内存公共内存中的变量产生了不同步的问题,进而导致数据不一致,所以呢,原来太要解决变量,它只是解决了读取时的可见性问题,它没有办法保证原子性,我们要修改这样的斜操作,必须要加lock,按lock或者S,那么好,我们再来详细的说明一下。来同学们,第一种情况啊,假设主物理内存这有个直视五,我们第一种情况大家都清楚,我们加S没问题吧,那么这个时候呢,你晓得的,这个时候我们是加锁了,那么加锁从头到尾有且仅有一个线程可以进来,同一时间下,那么这个时候铁定原子性,那么这个时候我们的操作是这样的,那么现在第一个线程。先过来读,从头到尾,从读到写,再写回加了synchize,保证可可见原则啊,OK,这是SYNCH听懂,那么这个时候我读过来这儿。
08:10
是个我。我这儿做的操作加个一,比如说number加加嘛,那么现在读到这儿的时候是个五。又做了加一操作,这个值将从五变成了多少,大家都清楚,是不是六,OK,那么这个时候我们就把这个六再由线程A写回进我们的。主内存共享变量,此时这个值就是个六,好解锁S完了,那么第二个线程B也过来,它读到的就是最新的值啊六,它也做一个加一的操作,那么现在六就变成了七,此时它写回主内存共享变量的,那么就是我们的这个最新值七,OK,这儿加一起效,这儿加一起效,也就是说各自的线程在自己本地内存。
09:00
他的自己的私域空间,他的工作内存里面做的操作,只要加一次就起效一次,并且能够成功解回,OK,这是我们的好,那么同学们这些我相信大家呢,都能明白过。我们下面的问题就来了啊。现在呢,我们恢复到我们的原貌,注意这原来加每一个加接加一次啊,它都兑现都是有效的,那现在不好意思啊,我们就变喽,我们现在呢,就变成了什么呢。我们没有SYNCH了,我们现在是好了。没有原子性,大家都可以去读,那么现在啊,还是熟悉的配方,还是熟悉的味道。这是五。现在大家呢,可以同时刷进来,没有原子性,那么这儿读到的也是我,这儿读到的也是我,OK就看谁快了,看CPU的调度,那么这我要加个一。
10:00
这我要加个一好,那么现在正常值是不是应该你加一个是六,我加一个应该是什么?六加上一变成七,正确的值应该是七吧,OK,也就是说两个加一都要生效,是加了两次操作,但是。这哥们提前先写回去了,好,这个五我这加了一以后变成了六,我写的比较快。我呢已经把值呢写回去,现在这个最新的值变成了六。线程B比较慢,那抱歉,由于Y跳的可见性,通知你主内存这个共享变量已经由A线程用最快的第一名它已经写回来了,是六,那么现在最新值是六,你手头上这个作废了,抱歉,哎,可是我在这辛辛苦苦的加载计算赋值这三步操作是非原子的啊,那这个时候我这儿加了个一了呀,我也做了操作,我自己这儿也变成了六了。但是抱歉,你这个一相当于什么作废了,他不承认你手头上这这你手头上的这次操作。
11:04
白操作了,OK,所以说呢,我呢不得不线程B。又回去主内存当中的共享变量读最新的值啊,这是几,现在读到我的工作内存里面是六好,我再做一次加一。现在是几是七,然后我再把这个值。写回来,此时它的值是多少?是七,那么现在大家请看一下,那么这就是可以解释了,为什么我们每次来运行这个程序。它永远是接近1万,永远到不了1万,都会发生数据的写丢,就是在这儿图,大家请看这个加一生效,这个加一失效,这个加一生效,那么最终起效果的只有两个,但实际而言,我们多线程的在自己的工作稳定内存里面,它做了三次操作,但是只有两次生效,这可不是就写丢了一次吗?所以说对于我们底子而言,这三步非原子的操作,它呢,我正在做着计算呢。
12:06
我这次劳动成果,也就是我们这儿的这次加击,由于外变量别人已经先提前写回了主内存单中,那么根据及时性、可见性的原则,我本次操作就作废了,也就是说我白忙活了,我本次劳动成果做了,但是根本不被计入,但是我们每个县城要做1000次,某一次的操作就这样白花花的。白费了,那么可不是,我表面上123写了三次,我计算了三次应该加三,但实际而言,这次作废了,或者某次作废了只写进去二,可不是产生了数据写丢失的问题。所以说我们这儿可以看得出各个线程的私有内存和主内存的公共变量中的变量不同步了,那么进而导致数据不一致,所以y tell确实满足了可见性,但是无法保证原子性,因为这个时候多个线程一起去群雄逐鹿,大家一起去抢,实像非常难看,数据的一致性,稳定性遭到了破坏,我们对于这样的场景一般必须要加速来保证同步,所以呢,我们呢可以获得它呢并不具备原子性。好那么如果你。
13:17
还是觉得不认可的话,我们再深一步,从字节码的角度来分析,那么弟兄们。对于我们这样的number加加,我们呢,用汇编加二的底层原始的命令我们都晓得啊,这我们节约时间啊,老师呢,已经提前做好了,都给你们抓了图了,也就是说对于我们的number加加这个操作表面上是一步,实际底层123分别是它被拆分成了三个指令。Get代表什么?拿到原始值number加加嘛,我先拿到。当前读进来它的值是多少,然后。对于I代表整形变量,对吧,进行加一操作,然后加完了以后再put写回进去。
14:01
这这三步没有洛克,按洛克或者think控制别的线程也可以发起操作,所以说这个时候我们就会明白。原子性我们指的是一个操作当中不可中断,必须延续,必须连续有且仅有一个来干那么多线程环节下面一个操作一旦开始就不应该被受到其他线程的影响,但是由于我们这并没用think修饰啊,所以这个爱加加操作底子分成三步,不具备原子性,那么相当于说他呢,先读在家再写回去干成了三步,那么。有。缝隙,容易被其他县城捷足先登。如果第二个线程在第一个线程读取旧值啊和写回新值期间读取了I的值啊,那么这个时候说明什么?第一个线程的那个操作还没写回来主内存里面呢,还是原来那个值啊,那么这样的话,我们两个看到的就是同一个值啊,其实质而言,它的值其实呢,还在运输的过程当中,计算的过程当中,实际上它应该被加一个,但是没有看到,那么他们两个会同时执行加一展的操作,也就造成了现成的安全失败,OK,那么我们要think来保证,所以呢,我们一定要明白。
15:13
回来看,我们的结论就是不适合参与到依赖当前值相关的运算,因为它不具备原子性。那么我们呢,来看看按照周志明老师深入Java虚拟机所给我们的建议,外来tell对于这样的I等于I加E或者I加加这样的符合连带操作的不要用。那么它适合用在哪呢?最适合用作保存某个状态的布尔值或者int值,为什么呀?比如说布尔值true还是副,只要一改变马上可以通知其他内存,那么这个时候是不是就是我们的可见性优势的利用啊?所以呢,我们按照深入Java虚拟机,那么现在我们得到的结论。为了太变量,只能保证可见性,在不符合以下两条原则的场景,只要不符合以下两条原则,我们也仍然要通过加锁来保证原子性,那么这两条原则也给大家写在这儿了,很清晰,那么我也就不在照本宣科,多废唇舌。好那么接下来我们念式的回答就是一句话,假设让你举一个外不具备原子性的案例,那么就对于I加加这样的符合操作,那么。
16:17
回来肽尔是不具备原子性的,那么在见习期不同步非原子操作,这样A加加会出现数据的问题。那么大家请看。我们都晓得主内存的一个变量,多个线程没有加锁,谁都可以来碰一下,那么对于V代表可见性,只要有人改了,马上其他线程收到最新通知啊,会去主内存当中去读最新数据红框框这块,那么到时什么读取到本地内存,可以保证加载时是最新的,但是这三步操作由于没加锁。在见习期容易出现第二个线程,在第一个线程读取旧值哎和写回新值期间,那么被第二个线程也偷机了,那么这个时候就会导致什么这三步非原子操作造成了线程安全问题,好,那所以说同学们我来跳不具备原子性,那么请答家。
17:08
实习。我们。
我来说两句