当前位置: 首页 > 科技观察

面试官:什么?SynchronousQueue是个铃铛?观点?房子?

时间:2023-03-21 19:33:24 科技观察

今天的文章,我们继续讲述刘建筑师的故事。有一段时间,大刘经常给一些程序员讲课。一方面是出于球队训练的需要,另一方面也是因为大刘本人要打凡尔赛,炫耀一下自己的实力。公司里的任何人都可以听刘老师讲课。提前一周在公司群里公布主题,谁想听日期就去吧。有一次,小刘在讲并发话题的时候,为了表明自己确实是一个并发开发的人,用了一个SynchronousQueue作为例子。他说这个队列其实没有volume的概念,就是线程持有的数据相互匹配。嗯,说到这里,还是要说一下,大刘其实对SynchronousQueue不是很了解。只是第一,这东西没人用,自然没人懂;其次,它的概念比较晦涩,有时甚至比较违反直觉,所以即使随便说了一句也未必是对的,也未必会被发现。它会给人一种无知的感觉。大刘用过几次,感觉不错。所以闲着没事就得showSynchronousQueue,说这么奇葩的人你懂的,开发者的名字也没有错。当时,茶茶被拆了。就在这时,班里来了一个新人。此人中等身材,相貌普通,脸上却像是种地多年的老农巴掌。脸上的痘痘就像老农手掌上的老茧。此人姓张。由于他的脸在这里看起来像一个大巴掌,所以暂且称他为巴掌张。这一巴掌打断了刘的话,一口咬定刘说的不对,他看到了这个SynchronousQueue,不是刘说的。大刘有些愧疚,脖子上渗出一圈汗珠,但又不想放弃未开发者的称号。于是他说了一大堆废话,就把话题扯到一边了。并告诉SlapZhang下次我们要在这个舞台上和他PK,一定要好好看看谁才是SynchronousQueue的真知音。大刘觉得自己的脸被打了一巴掌,他下定决心要好好研究一下SynchronousQueue。用谷歌和百度一起搜索,东西方结合,让外国的东西对中国有用,已经做了一段时间了。最后,角落里有一个破损的小网站。有人这样说:SynchronousQueue的目的就是为了连接和匹配。当连接起来后,双方合作愉快,整个工作就完成了。但是一旦到了接头,双方都没有到,那么对方就必须堵着等待。这句话瞬间敲了大刘的脑袋,让聪明的智商重新夺回了制高点。为什么这句话点亮了大刘本就如同电灯泡一般的脑袋?因为大刘每次都记得自己的采访经历,和这次的联名一样。每次刘某去面试,他都非常客气地赶到新公司。但大多数情况下,时间一到就需要很长时间才能开始面试。大刘那时候年纪也小,只是以为领导有事,就恭恭敬敬的等着。直到刘自己当上了leader,去面试别人,被HR温柔的提醒,让应聘者稍等片刻再去。可见公司很忙,应聘者应该对公司保持一定的敬畏心。当时大刘就知道这是一种PUA技术……大刘对比了一下自己的面试经历,一下子明白了SynchronousQueue的概念。SynchronousQueue本身就是为了切换和匹配而存在的。当一个线程往SynchronousQueue中放入东西,发现没有线程在等着拿,就阻塞——这就像面试官早早来了,等待面试官。当一个线程去SynchronousQueue拿东西,发现什么都没有的时候,它就等待——就像面试官早到,等待面试官。理解SynchronousQueue的时候,是冬天,屋外寒风凛冽,屋内大刘灯火通明。它只是堂而皇之地放在JDK底层并发包中的一个队列结构。当然,SynchronousQueue没那么简单,里面还有几十亿的细节。所以,刘在了解了大方向之后,就开始研究细节。他要努力打压张巴掌的嚣张气焰,而大刘要成为公司技术的头牌。回到现实,SynchronousQueue的真正目的是让两个线程的工作结果交接。这没有错。不过,这次交接是严格保密的,任何人都不能窥视。嗯,没错,不能像约了女朋友去钟点房一样被人偷窥。好了,围绕着这个SynchronousQueue的小时房,我们通过源码来看一下细节。首先,钟点房是严格保密的,谁也不知道里面有多少人。因此,我们不能允许他人通过方法获取特定数据。对于SynchronousQueue,自然是无法通过size()获取到任何信息。/***总是返回零。*A{@codeSynchronousQueue}没有内部容量。**@returnzero*/publicintsize(){return0;}/***总是返回{@codetrue}。*A{@codeSynchronousQueue}没有内部容量。**@return{@codetrue}*/publicbooleanisEmpty(){returntrue;}其次,钟点房不能随便进去查房看看是谁。所以,自然是不能迭代了。/***Returnsanemptyiteratorinwhich{@codehasNext}总是返回*{@codefalse}。**@returnanemptyiterator*/publicIteratoriterator(){returnCollections.emptyIterator();}同样,计时室保护隐私,它也不让你钻不好意思,我不告诉你XXX是不是躲在小时房里了。所以,你无法知道钟点房里有没有这样的人。/***总是返回{@codefalse}。*A{@codeSynchronousQueue}没有内部容量。**@paramotheelement*@return{@codefalse}*/publicbooleancontains(Objecto){returnfalse;}/***Returns{@codefalse}除非给定的集合是空的。*A{@codeSynchronousQueue}hasnointernalcapacity.**@paramcthecollection*@return{@codefalse}unlessgivencollectionisempty*/publicbooleancontainsAll(Collectionc){returnc.isEmpty();}钟点房自然无权赶人出去./***总是返回{@codefalse}。*A{@codeSynchronousQueue}没有内部容量。**@paramotheelementtoremove*@return{@codefalse}*/publicbooleanremove(Objecto){returnfalse;}当然,作为商业钟点房,SynchronousQueue还是很注重安全的,贴心的提供了紧急转运的手段。/***@throwsUnsupportedOperationException{@inheritDoc}*@throwsClassCastException{@inheritDoc}*@throwsNullPointerException{@inheritDoc}*@throwsIllegalArgumentException{@inheritDoc}*/publicintdrainTo(Collectionc){if(c==null)thrownewNullPointerException();if(c==this)thrownewIllegalArgumentException();intn=0;for(Ee;(e=poll())!=null;){c.add(e);++n;}returnn;}/***@throwsUnsupportedOperationException{@inheritDoc}*@throwsClassCastException{@inheritDoc}*@throwsNullPointerException{@inheritDoc}*@throwsIllegalArgumentException{@inheritDoc}*/publicintdrainTo(Collectionc,intmaxElements){if(c==null)thrownewNullPointerException();if(c==this)thrownewIllegalArgumentException();intn=0;for(Ee;n传输器;/***用于双堆栈和队列的共享内部API。*/abstractstaticclassTransfererform{/***P.**@parameifnon-null,theitemtobehandedtoaconsumer;*ifnull,requeststhattransferreturnanitem*offeredbyproducer.*@paramtimedifthisoperationshouldtimeout*@paramnanosthetimeout,innanoseconds*@returnifnon-null,theitemprovidedorreceived;ifnull,*theoperationfailedduetotimeoutorinterrupt--*thecallercandistinguishwhichoftheseoccurred*bycheckingThread.interrupted.*/abstractEtransfer(ee,booleantitimed,longnanos);}Transfer经理每次开门营业,都会收到总部的告示,告诉他要注意管理方式,比如公平有效,比如giv优先考虑贵宾等。/***默认为VIP客人开一个后门*/publicSynchronousQueue(){this(false);}/***总部递牌子告诉Transfer是公平还是不公平,*/publicSynchronousQueue(booleanfair){transferer=fair?newTransferQueue():newTransferStack();}先来看看适合上班族的fair模型,先到先得,迟到者不打折。staticfinalclassTransferQueueextendsTransferer{staticfinalclassQNode{...}transientvolatileQNodehead;transientvolatileQNodehead;transientvolatileQNodecleanMe;TransferQueue(){//经典链表例程,先创建一个虚拟头节点QNodeh=newQNode(null,false);head=h;tail=h;}…………QNode是Transfermanager需要的牌子,上面记录了一些信息,到时候不要搞错。staticfinalclassQNode{volatileQNodenext;//队列中的下一个小伙伴volatileObjectitem;//这次小伙伴带来了要交的东西volatileThreadwaiter;//交接线程finalbooleanisData;//isData==true表示它有东西QNode(Objectitem,booleanisData){this.item=item;this.isData=isData;}//...省略一系列CAS方法}要做什么,秘诀就在transfer()。@SuppressWarnings("unchecked")Etransfer(Ee,booleanimed,longnanos){//...先省略细节}转账的本质是等待转账完成或者转账中断,取消,或者等待超时。for(;;){QNodet=tail;QNodeh=head;//因为初始化是在构造函数中完成的,所以可能在构造函数执行之前使用,t或h会为nullif(t==null||h==null)continue;//不能做任何事情//h==t表示没有人,t.isData==isData表示过来的哥们和前面的哥们目的一样,所以才可以考虑排队等候。if(h==t||t.isData==isData){QNodetn=t.next;//需要考虑线程不安全,当前tail错误,错误,重新确认if(t!=tail)continue;//确定队伍的尾巴,发现又来人了,所以把尾巴指向新人if(tn!=null){advanceTail(t,tn);continue;}//超时,不要等待if(timed&&nanos<=0)returnnull;//终于可以了,小伙伴们可以注册进屋了if(s==null)s=newQNode(e,isData);//有人可能会跳进去中间,所以只能等待if(!t.casNext(null,s))continue;//等待预约的人advanceTail(t,s);Objectx=awaitFulfill(s,e,timed,nanos);//同一个人出来,就是Thetaskfailedif(x==s){//cleanupclean(t,s);returnnull;}if(!s.isOffList()){//haven't出队advanceHead(t,s);//在行前单独处理if(x!=null)//设置标记成功s.item=s;s.waiter=null;}return(x!=null)?(E)x:e;这一段是否正确?看起来很头疼?其实Transfer也很头疼。首先要面对第一个问题:资源竞争问题。顾客络绎不绝。由于Transfer强迫症,他认为每次都要从线的绝对头部或尾部开始。因此,他每次都要判断自己看到的线头是真实的还是线尾是真实的。队头,队尾。确认没有问题后,才开始将新来的客人做成真正的团队尾巴。那么,成为团队尾巴的哥们就可以等着自己的Mr.Right上门交接了。等待切换成功或失败的方式是awaitFulfill(t,tn)。这里有人在等着,与此同时,交接的人也开始陆续的走了过来。else{//complementary-modeQNodem=h.next;//nodetofulfillif(t!=tail||m==null||h!=head)continue;//inconsistentreadObjectx=m.item;if(isData==(x!=null)||//malreadyfulfilledx==m||//mcancelled!m.casItem(x,e)){//交接核心语句advanceHead(h,m);//dequeueandretrycontinue;}advanceHead(h,m);//成功实现LockSupport.unpark(m.waiter);return(x!=null)?(E)x:e;}交接的核心其实是m.casItem(x,e)。交接成功,大家各自返回家园。整体流程如下:一开始是一个经典的链表开始,head=tail开始依次有节点链接,put的时候,isData=true;take时,isData=false,可能同时有多个put操作,没有对应的take操作,将它们一个一个链接起来,形成一个链表,通过awaitFulfill方法等待对应的take.也可能同时有很多take操作,但是没有相应的put操作。puttake操作会从链表的头部开始寻找匹配的put,然后通过casItem方法交出put操作。put操作会从链表的头部找到匹配的take,然后通过casItem方法交给它。所以,正如你所见,SynchronousQueue是专门针对交接任务的。putbuddy发现没有人take,就在原地等着,等待take操作。没有人放的时候take小伙伴就在那里等着,等待put操作。这就是我们的SynchronousQueue钟点房所做的事情。OK,钟点房既然营业了,那也是要赚钱的。所以,它不得不搞VIP客户收费,它不得不对VIP客户提供一些优惠。对于这些VIP客人,我们的Transfermanager会专门以stack的形式安排客人,来的客人越多,牌子就越大。所以,自然是后来的客人会优先交接。这里简单介绍一下,就不赘述了。Transfer成为TransferStack,后来成为优先服务。开头自然是链表的开头。无意义的链表头指向null,链表为空。客官二话不说,你先来。和TransferQueue一样,如果都是take,mode是REQUEST,就得排队交接的人出现了,哥们可以接档了,其他的不说了,一样的,说多了没意思,之后大刘摸清了这些底细,第二天,当张巴掌再次挑衅的时候,大刘彻底稳住了。详细情况一一解释清楚后,看着巴掌落寞的脸庞,瞬间感觉不是巴掌,而是剪刀石头布。大刘按捺不住,对着这个布比拔了剪刀,光荣地结束了这场战斗。刘仍然主导技术流程。下期大刘的故事见。本文转载自微信公众号“思源外”,可通过以下二维码关注。转载本文请联系思源外公众号