你應(yīng)該這樣去開(kāi)發(fā)接口:Java多線程并行計(jì)算
所謂的高并發(fā)除了在架構(gòu)上的高屋建瓴,還得需要開(kāi)發(fā)人員在具體業(yè)務(wù)開(kāi)發(fā)中注重自己的每一行代碼、每一個(gè)細(xì)節(jié),面子有的同時(shí),更重要的還是要有里子。
面對(duì)性能,我們一定要有自己的工匠精神,不可以對(duì)任何一行代碼妥協(xié)!
今天和大家分享在業(yè)務(wù)開(kāi)發(fā)中如何降低接口響應(yīng)時(shí)間的一個(gè)小技巧,也是大家日常開(kāi)發(fā)中比較普遍存在的一個(gè)問(wèn)題,即如何提高程序的并行計(jì)算能力?
本文主要包含以下內(nèi)容:
- 順序執(zhí)行很慢
- 線程池+Future并行計(jì)算
- 使用Java8的CompletableFuture
- 使用Guava的ListenableFuture
本文包含代碼內(nèi)容較多,大家可收藏后自己跟著動(dòng)手驗(yàn)證一番~
順序執(zhí)行
很多時(shí)候,我們開(kāi)發(fā)一個(gè)接口時(shí)候,需要調(diào)用多個(gè)方法,然后將各個(gè)方法返回的數(shù)據(jù)一起組裝返回給前端,比如這樣的:

可以看到我這里調(diào)用了4個(gè)方法,每一個(gè)方法為模擬真實(shí)耗時(shí),所以都是延遲100ms返回一個(gè)字符串:

可想而知,我們這個(gè)接口的響應(yīng)時(shí)間一定會(huì)超過(guò)400ms,多次執(zhí)行都會(huì)在400ms多一點(diǎn):
耗時(shí):403ms耗時(shí):409ms耗時(shí):406ms
這就是順序執(zhí)行,也許大家覺(jué)得很Low,但是想想自己的代碼很多時(shí)候不就是這樣子的么?
線程池+Future并行計(jì)算
順序執(zhí)行確實(shí)很慢,所以我們需要并行執(zhí)行,即同時(shí)調(diào)用這四個(gè)方法,熟悉Java多線程的都知道,每個(gè)方法單獨(dú)開(kāi)啟一個(gè)線程異步去執(zhí)行就好了,等全部執(zhí)行完了拿到獨(dú)立線程執(zhí)行的結(jié)果再組裝起來(lái)就可以了。
但是每次調(diào)用都需要?jiǎng)?chuàng)建四個(gè)線程,線程的創(chuàng)建和銷(xiāo)毀都是需要開(kāi)銷(xiāo)的,所以我們就有了池化技術(shù)。
線程池、數(shù)據(jù)庫(kù)的連接池等都是采用的池化技術(shù):預(yù)先初始生成創(chuàng)建好的線程,等需要調(diào)用的時(shí)候拿來(lái)即用,線程完成工作后回歸空閑狀態(tài),等待下一次任務(wù)的到來(lái),這樣就避免了線程頻繁的創(chuàng)建、銷(xiāo)毀,提高了程序的響應(yīng)性能。
所以我們?cè)谧霾⑿杏?jì)算的時(shí)候一定要充分的利用線程池的相關(guān)技術(shù),關(guān)于線程池的技術(shù)在我的另外一篇文章單獨(dú)講到,不了解的同學(xué)可以初步了解一下,面試也是必會(huì)題之一:
下面我們直接上代碼:

線程池+Future
多運(yùn)行幾次,看輸出響應(yīng)時(shí)間:
耗時(shí):108ms耗時(shí):105ms耗時(shí):105ms
效果是不是很明顯?
直接相當(dāng)于一個(gè)方法的調(diào)用耗時(shí),實(shí)際開(kāi)發(fā)中如果你的一個(gè)接口經(jīng)過(guò)壓測(cè)耗時(shí)在100ms左右(大多數(shù)正規(guī)公司對(duì)接口性能都會(huì)要求不超過(guò)100ms),那么再通過(guò)線程池+Future并行計(jì)算的方式,并可以瞬間將你的接口性能提高上去,再也不用擔(dān)心壓測(cè)不過(guò)了。
有時(shí)候測(cè)試同學(xué)告訴你接口壓測(cè)不過(guò)是不是覺(jué)得很沒(méi)面子?那是對(duì)你職業(yè)水平很大的否定~
Java8的CompletableFuture
Future是java.util.concurrent并發(fā)包中的接口類(lèi),用來(lái)表示一個(gè)線程異步執(zhí)行后的結(jié)果,有如下核心方法:
- Future.get():阻塞調(diào)用線程,直到計(jì)算結(jié)果返回
- Future.isDone():判斷線程是否執(zhí)行完畢
- Future.cancel():取消當(dāng)前線程的執(zhí)行
我們可以知道的是,F(xiàn)uture.get()是阻塞調(diào)用的,要想拿到線程執(zhí)行的結(jié)果,必須是Future.get()阻塞或者while(Future.isDone())輪詢(xún)方式調(diào)用。這種方式叫“主動(dòng)拉(pull)”,現(xiàn)在都流行響應(yīng)式編程,即“主動(dòng)推(push)”的方式,當(dāng)線程執(zhí)行完了,你告訴我就好了。
Java8設(shè)計(jì)了CompletableFuture這樣的一個(gè)類(lèi),我們先來(lái)看看如何用CompletableFuture來(lái)開(kāi)發(fā)之前的代碼:

CompletableFuture并行計(jì)算
這里可以看到實(shí)現(xiàn)方式和Future并沒(méi)有什么不同,但是CompletableFuture提供了很多方便的方法,比如代碼中的allOf,thenApplyAsync,可以將多個(gè)CompletableFuture組合成一個(gè)CompletableFuture,最后調(diào)用join方法阻塞拿到結(jié)果。多次調(diào)用該接口耗時(shí)如下:
耗時(shí):110ms耗時(shí):108ms耗時(shí):105ms
CompletableFuture類(lèi)中有很多的方法(50+)可以供大家使用,不像Future只要那么幾個(gè)方法可以使用,這也是Java自有庫(kù)對(duì)Future的一個(gè)增強(qiáng)。
這里只是簡(jiǎn)單展示了CompletableFuture的一種用法,實(shí)際開(kāi)發(fā)中大家需要根據(jù)不同的場(chǎng)景去選擇使用不同的方法,這里對(duì)API不做具體介紹了。
Guava的ListenableFuture
總是有一些牛逼的公司牛逼的人出一些牛逼的開(kāi)源組件要比官方自帶的工具類(lèi)要好得多,同樣,谷歌開(kāi)源的Guava中的ListenableFuture接口對(duì)java自帶的Future接口做了進(jìn)一步拓展,并且提供了靜態(tài)工具類(lèi)Futures。
針對(duì)上面的代碼,我們看如何使用ListenableFuture來(lái)實(shí)現(xiàn)(與之前不同的是,Guava中需要對(duì)線程池再進(jìn)行一次包裝):

執(zhí)行三次請(qǐng)求耗時(shí):
耗時(shí):103ms耗時(shí):101ms耗時(shí):103ms
最后
以上就是如何讓自己的接口并行計(jì)算起來(lái)的三種實(shí)現(xiàn)方式,屬于日常開(kāi)發(fā)中比較常用的一個(gè)小技巧,這里沒(méi)有過(guò)多說(shuō)明這三種方式的具體區(qū)別,實(shí)際上還需要大家不斷的在開(kāi)發(fā)中去使用,查閱更多相關(guān)源碼和資料,只有等你真正用起來(lái)的時(shí)候,你才能有所體會(huì)!