00:00
到现在为止呢,是你月薪40K大概需要掌握的内容。听明白我意思了吗?好,有同学说老师这个月薪40K,需要我掌握内容是什么样的一个东西呢?呃,美团遇到著名的问题。TCL单利要不要加?这是美团一道著名的问题,有同学可能听到过了,听到过你可以撤了啊,拜拜。有同学没听过,没听过的给老师扣个一。喝口水,我们继续。来杯水水,嗯。这玩意儿好吧,好不废话了啊,我们继续来看这这这道这道面试题,我们来聊一聊,这道面试题比较难啊,为什么说他是呃,月薪超过40K的这样的一道面试题呢?因为它里面包含的概念呢非常多,这块能听懂就听,听不懂就算啊,我们先来聊这个DC这事好吧。
01:13
呃,先来聊单利这事,再来聊DC这事,再来聊要不要加VO这事,嗯。秦建,你不是VIP吗?不要听这个课了,你现在还没到40万的,就是就是年薪50万的这个这个程度,先从年薪十十几万开始,找咱们张山老师去好吧,好看这里啊,咱们先聊什么叫单利这件事,呃,单利这件事。忘忘了忘了忘了忘了,把这个那什么给打开,嗯。单利,呃。单利这件事需要我解释吗?需要解释的给老师扣个二。丹丽。
02:02
嗯。嗯。先把这个项目给打开啊。最近我重装了一下那什么。发open。找到我们的项目。呃,这个呢,是我在讲这个设计模式的时候讲到的关于单利的内容open。单利设计模式。New window。
03:02
Project。Source Java,嗯,设计模式呢,23种,我基本上给大家讲的非常的齐,在这里面呢,我们拎出单立来啊,单立在哪里。Sing。嗯,看这里好,大家听我说啊,就是单利这件事是一个什么事呢?其实它非常的简单,就是一种设计模式。设计模式应该都听说过是吧?嗯,至少你就算没学过,应该知道他的名字,大名鼎鼎,可以这么说啊,你想你要想读源码,读懂别人的写的源码设计模式必须得学,不然你看不懂别人源码怎么写的。好,大家看这里啊,所谓的单利指的是什么?就是这个类,这个类MANAGER01这样一个类。要求它只能在内存里头只有一个对象,你谬它的时候不能留第二个。但这事怎么实现呢?最简单的实现方式是这样的。就是我上来二话不说,我只我自己先拗一个啊,我拗了一个卖点零一。
04:05
变量名叫instance,然后呢,我把它的构造方法设成private,那不让别人扭,只让我自己扭。那别人就不了了,那么我我我我该怎么办呢?我如果想用这个实例怎么办,我对外呢提供一些方法,那这个方法呢,就是get instance,然后有任何人想用我这个对象的时候都得这么写。me01.get instance me01.get instance,当然这哥俩呢,用的拿到的一定是同一个对象啊,所以最后我们判断M1到M2一定是等于出,这是单立的概念。来还有没还有没有人不明白的,这就是单例。嗯,给个高并发的单例看,看来这个就是高并发的单例。这是最简单的写法,也是能支撑高并发的,也是能保证单力的,所以一般的情况下,没事儿的时候就用这个。
05:06
不过有的人呢,他是属于喜欢挑毛病的人。嗯,你懂吗?他是喜欢挑毛病的人,什么叫喜欢挑毛病的人呢?比如说处女座。双鱼座。巨蟹座。和其他的九个星座,这些都是喜欢挑毛病的人,OK,开个玩笑啊,然后呢,呃,挑毛病的时候他是这么挑的,他说你这个对象呢,我还没有用到你的时候,什么叫我还没有用到你啊,就是我还没有调这get instance,你老人家二话不说,先占了块空间,我内存里头要是。弄出一块空间来,我还没用到你呢,还没没叫你呢,你先进来给我,把这个地儿给我粘了,给我坐在这儿了,太讨厌了,能不能等我用你的时候你再进来,等我用到你的时候你再给我拗可以吗?所以就诞生了这种写法,这种写法非常简单啊,看这里,呃,依然是这个get instance方法,等你我刚开始的时候不把它出来,等你调用get的时候,我再把你出来,我当然这时候我要做个判断。
06:20
哎,你是不是依然为空呢,你还没进来呢,对不对,没进来的时候我就把你拧出来,等你进来了,我直接拿来用就行了,好,这是第二种写法,第一种写法呢,叫恶汉式,就上来不二话不说,先吃了,这种呢叫懒汉式,什么时候用我的时候,什么时候我才进来。好吧。呃,但是这种方法呢,也有他的毛病,毛病在哪里?毛病就在于多线程访问的时候,它依然会产生好多个新对象,为什么第一个线程来了,看诶,等空吗?等空,第一个线程暂停,第二线程来了,等空吗?依然等空,结果第一个线程继续运行,扭了个对象,第二线程继续运行,又扭了个对象,所以有可能你拿到的不是同一个对象,保证不了单立,这是他的毛病,在多线程情况下保证不了单立。
07:12
呃,这块能能get到的,给老师扣个一可以继续吗?嗯,比较简单啊,怎么解决?解决办法非常的简单了,就是你加一个synchronized synchronize的概念是什么?Snchize的概念是当我持有这把锁执行下面这个操作的时候,所有其他线程必须等,得等我执行完了,你才能够顺序执行,所以把多线程改成了单线程。彩票了,对啊。这就是白羊座的特点。太挑了,嗯,好看这里啊,所以这个是。当然,就作为这个S来说,这把锁又有人挑毛病。嗯。现在是天秤座的人过来挑毛病了,他说这这个,万一我里边写了很多业务逻辑,你在锁定这些代码的时候,把我这些没有必要锁定的业务逻辑也给锁定了,你这个锁的力度。
08:12
好,这个东西呢,叫锁的力度。这个锁的力度太粗了。太粗了,不够优雅。对吧,你看天秤只看优不优雅,你太粗了,不够优雅,所以能不能把这个锁的力度给它细化。好。所以就诞生了后面这种写法,这个写法也很简单,看这里,我上来之后get instance方法,我先判断哥们儿,你是不是等空了,你还没进来,我上锁。我不会把整整个锁上,在我整个方法上,所以呃,我判断你为空的时候,我再上锁,我上完锁把你给扭出来,扭出来之后呢,我锁就释放了。好,能看懂这意思啊,这就叫锁的力度给细化了。这是天秤座的写法。
09:01
后来天蝎座的人过来了。他阴用阴险。狡诈。黑暗的眼神盯了一会儿,说,有毛病。毛病在哪儿?还告诉我毛病在哪儿。这个东西能保证只有一个多线程访问的时候真的只有一个实例吗?能不能?还是不能。为什么呀,我不是上锁了吗?哎,我不是上锁了吗?还是不能。我们先来证明它一下,好吧,下面我起了100个线程,在这100个线程里边,嗯。我掉了他的get instance,然后呢,打印它的hash code,如果是同一个hash code一定是一样的,我们跑一下。好,看这里你会发现肯定不是同一个啊,这是这就呃,说明了多线能底下有问题,为什么会长出我题,你们分析一下。
10:09
这个特别简单,第一个线程来了,判断哥们儿你是等空,好,第一个线程在这停了,第二线程来了,哥们儿你依然为空,然后第二线程上锁。有对象解锁。好,第一个线程继续运行。他能上锁吗?当然可以,因为你第二个线程已经解锁了,我当然可以上,所以他又来一遍上锁。拗对象解锁,这哥俩呢,拗了俩对象。不是同一个。好嘞。所以这个方式依然有问题,因此就诞生了这种写法,这种写法我希望大家背过,这种写法希望大家背过,注意啊,这种写法呢,并不是一定是在单例里面,在很多很多的程序里面都是这种写法,这个写法是这样来做的,就是我上来先判断哥们儿诶。你是不是等空了,如果你等空。
11:02
上锁,上完锁之后我再判断一遍,你是不是依然为空?哥们儿,你是不是依然为空?如果你依然为空,说明我在上锁的过程中,另外一个线程没有把你给扭出来。这样我就保证了多线程的安全性,数据的一致性。OK。再看一遍,上来先判断是不是等空,如果等空上锁,上完锁之后我再检查一遍你哥们,你是不是在我上锁的这个过程之中没有被别人改变过,那么这个时候你依然为空。Very good,这个叫依然为空。然后这时候我就把它扭出来。嗯。当然这是这种呢,就叫做DC。DCL什么意思呀?叫double check lock。叫呃,两次检查中间加了一个lock啊,这检查一次,哎,Lock一下,这检查又一次,这叫DC,这样就保证了整个数据的一致性,这块没问题吧,叫双双重判定,好这写法能get到的来给老师扣个一。
12:17
好,当然这种写法呢,大家伙呢,会会有好多好多的问题,我给你讲几个听啊,讲的比较好玩的听。嗯,讲的比较好玩的,听啊听我说。第一个问题呢?总有同学会问。这外面这层判断有必要吗?我上来直接上锁再判断,等空不就完了吗?其实如果说你上来直接上锁的话,这个判断甚至都不需要。呃。啊,这个判断得需要,Sorry,这个判断得需要,对不起啊,就是说呃,如果你呃外面这层判断给去掉的话。它会产生什么问题啊。外面这层判断如果要去掉的话。
13:03
会产生效率变低的问题,外面这层判断是会提高效率的,举个最简单例子就行了。上锁锁竞争这件事情是需要消耗很大资源的。如果说判断需要一个纳秒,所竞争这个没有100个纳秒肯定拿不下来。所以这句话和这句话相比,这句话执行起来速度特别慢,那么听我说你你有1万个线程,这其中呢,只有一个线程把这个对象给溜出来了,另外所有的线程如果不不写这个判断。是不都得这9999个县城都得去有一个所竞争的执行过程啊。所以。外面这层判断是必须的。这部分叫提高效率,用。来get到的老师扣个一。嗯。
14:01
好嘞。呃,理解了这个问题之后呢,我们来来聊第二个问题啊,第二个问题也特别好玩。第二个问题是你这个有的同学不知道有没有同学想到过老师,会不会发生这样一种情形呢?这样一种情形就是。我首先判断它等空。然后我上锁。但是在我上完锁之后的这段时间里头。有一个哥们儿把他给拗了出来,另外一个哥们儿把他又制为空。能理解吗?就这哥们儿呢,原来是空置,但是呢,哎,原来是个空置,有哥们把它给拗了,拗完用完了,用完了之后又把它设为空了,如果我们程序随意写的话。是完全有可能产生这种情况的。有没有可能?是不是录播,对,就是录播,嗯,作为一个985的学生没错。
15:05
有可能吗?有可能有同学跟我抬杠说,老师这上锁了,没可能。上锁代码和不上锁代码,人家有有那种不上锁代码,有可能做这件事。为啥还能变为no?这不废话,你直接写instance等于no,它不就变成no了吗?对,录播还当人工智能没错,嗯。这还用问?好,这种呢,其实是其实是一个著名的问题啊,这个问题叫ABA问题。就是在我上锁的过程中,有可能别人呢,把我这个值,我期望它确实是个A值,哎,但是呢,却确实是望它依然为空,但是呢,这个值呢,有可能是中间产生了变化的,也有从为空变为扭出来,对象修改里面值又又设为空,当然其实如果中间有这么一个过程,对我们来说不影响操作。
16:01
也没关系。但如果影响呢?如果影响的话,你就得干另外一件事,得加版本号啊,你举个最简单例子啊,举个最简单例子。你你你,你跟你女朋友分手了是吧?啊,你,你走了,去寻找你自己的幸福,你女朋友在这里一个人黯然哭泣。呃,当你回来再去访问她的时候,你发现你女朋友依然是你的那个女朋友。但是。如果你给你女朋友加了一个版本号的话。实际上你会发现。在你走的时候,脑门上写了个1.0,在你回来的时候,脑门上写了是个99.0,它中间经历了一些故事。这就叫AB bbbbb BA a问题。那当然,如果你不在乎中间这个过程,其实也没关系。啊。该到了吗?当然这是那个另外一个概念,就是所里面的CAS,经常被问到的问题,就是ABA问题,这里面其实也有,但是只不过这套代码呢,只有我们自己去访问,也没有别的地儿说访问这个instance,所以这块其实是不存在这个问题的啊,是拓展一下好吧。
17:14
好,我们可以继续吗?嗯。嗯。好了。好,我们继续讲到这儿,很多同学可能已经忘了我的问题了,不知道大家有没有印象,都忘了我的问题了是吧?这问题是啥来着?就是这个东西啊,要不要加条。我靠,好,我讲明白DC了,下面我们要讲条的问题了,好好听,好好听。结论是肯定是要要的,为什么要,关键是为什么要DCL你们记住了,Volatile你们记住了,对吧?
18:01
现在这俩哥们为什么要结合起来呢?看这里。还得要解释一个小问题。看这里来这么一个这个小程序,哇,这小程序好难跑一下。这还有个throws exception去掉。来这么一小程序啊。来这小程序,能看懂的给老师扣个一。OK。小程序能看懂的给老师扣个一。Object object,有的同学说,老师,你逗我玩呢。哎,这东西呢,有它背后的含义。我们下面就来看一看,我们都知道啊,作为一个Java程序来说,它是解释执行的,这个这哥们呢,执行的时候呢,会被翻译成为class文件啊,这个肯定是大家都知道,那么这个class文件里面装的是什么东西呢?装的是JVM级别的汇编语言,就是它叫做二进制码by code二进制码,那么实际上呢,Java虚拟机呢,它是一个虚拟的一台机器。
19:18
作为英特尔的机器,它有它的汇编语言,作为Java虚拟机也有它的统一的汇编语言。好,这个语言是什么呢?就是二级制码的语言,就叫做code,我们可以观察一下这句话,执行完成之后的code view。Code。看着你。找到我们的main函数,Main,看它的code。拖过来。注意看啊,这里就是这个code,就是我们这句话等于new code,这个by code呢,大概有这么五条指令构成,分别叫new,叫duplicate,叫invo special,叫a store和return,呃,这几条指令呢。
20:02
这字字数太小了。我我给你复制到这里,好吧,复制到这里。好,大家注意看啊。呃,基本上我们要拗一个对象的时候,比如说我们这有克拉斯。那它呢,由于常见变量小M,我们要复制这个,因为六这个对象的时候这么写,T,小T等于6T。这个大家都知道啊,这里还有一个指针,就是这个引用这个小T,然后它会指向拗出来的一大块对象,就是这个大T。好,那真正拗的过程呢,是由这几步构成的。是由下面这几步构成。下面这几步呢,主要的有这么几步,New指令,In special指令和store指令,Duplicate跟这没关系,Return跟这个关系也不大。
21:08
所以我们主要看这三条指令,这三条指令是怎么执行的。New指令是申请内存。就是说这个对象到底占多大个,我先把这块内存给申请了,第二我先占一下。但是需要大家注意的是,一旦你申请了这块内存之后,它里边是有个成员变量的,这个成员变量是M,那么这个M的值是几呢?需要注意,这个M的值是零,M值是零。因为你还没有调用下面这条指令,这条指令叫in special,叫构造方法调用,调用构造方法只有调用完这条指令之后,这个M的值才会变成八。而最后这条指令A是从这个小T和我们这个对象建立关联,再来看一遍啊,再来看一遍就是六个这个对象在底层执行的时候,实际上它是比较复杂的,怎么怎么复杂呢,看这里。
22:03
当我们执行到60条指令的时候。它是申请这块内存,内存里面先把这个M设为零,它叫这个叫成员变量的默认值。清零。然后。调用到special构造方法的时候,才会把这个默认值变成初始值,变成八。调用a store的时候才会建立关联,所以它有三步构成,默认值、初始值,建立关联。好,这三步来,能跟上的给老师扣个一。嗯,好嘞,Very good,很好。那有同学就会说了,老师你在逗我玩呢,你这东西你讲了半天也没讲的面试题呢。这跟那个volunteer有关系吗?跟DC有关系吗?我们呢,把DCL这段代码复制过来啊。
23:02
BC这段代码复制一下。好,这是我们DC的代码,把它复制一下,避免你忘了。嗯,看这里啊,这是的代码你别忘了啊。注意看注意看,嗯,需需不需要加water,必须要加,为什么要加,如果不加的话会发生什么情况,大家看这里啊,作为一个现场来说,在呃控制这个单立的时候,作为现场来说,首先上来先判断if in空。OK,看一眼,所以第一个线程它会先判断instant等空,然后第一个线程会干嘛,会上锁,上一把锁,上完锁之后呢,会拗拗对象。
24:09
好,我们我现在解释的呢,是这句话,就instance等于new manage06如果new这句话刚才咱们分析过,它有三步,构成哪三步呢?就是先申请内存。所以呢,他先申请了这块内存,注意这块内存里面这个它如果有成员变量的话,这个成员变量是一个默认值。能看懂吧,嗯。这个成员变量是一个默认值,所以它扭到一半的时候。还没有建立关联,注意。后面这两条指令发生了重排序。重排序,那咱们说说重排序这问题。后面这两个等发生重排序,重排序就会发生什么。哎,这哥俩换了个顺序。再看一遍。当我们第一个线程。
25:00
拗到一半的时候,上来之后判断为空,为空之后上锁,上完锁之后依然为空,依然为空。开始拗对象,拗到了一半的时候。Only one only one,好,六到一半的时候,扭到一半的时候,这哥俩发生重排序,还记得这哥俩的作用吗?这哥俩的A的作用是建立关联,所以当我们执行到这句话的时候。他就已经提前建立了关联。指令重买,可是提前建立关联之后呢,你就会发现。这个是一个很严重的事情,为什么?因为这个小T,第一它已经不再是空值了,它也是not now不是空值了,第二,它指向的是一个初始化一半了的对象,半初始化。能看懂吗?好,接下来第二个线程来了,我们来看第二个线程。
26:02
第二线程来了之后,第二线程也是会判断,哥们儿你是不是等空,是这样的吧,他会看看这句话啊,哥们儿你是不是等空。可是同学们告诉我,这哥们等哭吗?等不等空,刚才我们说过他不等空,不等空,我还需要上锁去扭它吗?不需要。我是不是直接就拿来用啊好,你看看他用到的这个对象的这个状态初始化了一半的对象。好,所以这哥们初始化了一半,你就拿来用了,如果这哥们里边正好记录的是你成交了多少个订单,双11啊,100万的订单啊,突然之间回零了,太棒了。太帅了,这个bug你要想调,累死你,你调不出来的。好了。我们稍微回顾一下。
27:01
我不知道你是不是能体会到,为什么这道题是年薪40万了啊,Sorry,年薪,呃,月薪月薪月薪4万了啊。同学们能体会到吗?来,能体会到的。给老师扣个一。好,我们说。
我来说两句