你們好,我是 why,歡迎來到我連續周更優質原創文章的第 60 篇。面試
老規矩,先來一個簡短的荒腔走板,給冰冷的技術文注入一絲色彩。編程
上面這圖是我五年前,在學校宿舍拍的。框架
前幾天因爲有點事情,打開了多年沒有打開的 QQ。而後忽然推送了一個「那年今日」發送的動態。異步
這張圖片就是那個動態裏面的。異步編程
2015 年 8 月的時候正是大三放暑假的時間,可是那個暑假我找了一個實習,因此暑假期間住在學校裏面。宿舍就我一我的。那個時候我徹底沒有意識到,這是我程序猿生涯的一個真正的開端,也是我學生時代提早結束的宣告。函數
8 月 5 日凌晨,一隻小貓忽然躥到了宿舍裏面,在宿舍裏面旁若無人的,像宿管阿姨同樣審查着一切東西。甚至直接跳到桌子上,看着我敲代碼。徹底不怕個人樣子。測試
因而我把它放到了個人自行車上,當模特拍了幾張照片。spa
初見這隻小貓時的那種驚喜我還記憶猶新,可是這波回憶殺給個人更大的衝擊是:原來,這件事已通過去五年了。線程
若是沒有QQ的這個提醒,你讓我想這件事是發生在何時的,個人第一反應確定是好多年前的事情了吧,慢慢咂摸以後有可能纔想起,原來是大三暑假的時候的事情,而後再仔細一算,原來是僅僅五年前的事情呀。3d
短短的五年怎麼發生了怎麼多事情啊,把這五年塞的滿滿當當的。
不知道爲何若是把人生求學、步入社會的各個階段分開來看,我每次回頭望的時候都感受這好像是別人的故事啊。
幸虧我本身記錄了下來,幸虧這真的是我本身的故事。
好了,說迴文章。
先去個人第一篇公衆號文章中拿張圖片:《Dubbo 2.7新特性之異步化改造》
這是 rpc 的四種調用方式:
文本主要分享這個 future 的調用方式,不講 Dubbo 框架,這裏只是一個引子。
談到 future 的時候你們都會想到異步編程。可是你仔細看框起來這裏:
客戶端線程調用 future.get() 方法的時候仍是會阻塞當前線程的。
我卻是以爲這充其量只能算一個閹割版的異步編程。
本文將帶你從閹割版的 future 聊到升級版的 Google Guava 的 future,最後談談增強版的 future 。
談到 Future 的時候,咱們基本上就會想到線程池,想到它的幾種提交方式。
先是最簡單的,execute 方式提交,不關心返回值的,直接往線程池裏面扔任務就完事:
public class JDKThreadPoolExecutorTest {
能夠看一下 execute 方法,接受一個 Runnable 方法,返回類型是 void:
而後是 submit 方法。你知道線程池有幾種 submit 方法嗎?
雖然你常常用,可是可能你歷來沒有關心過人家。呸,渣男:
有三種 submit。這三種按照提交任務的類型來算分爲兩個類型。
可是返回值都是 Future,這纔是咱們關心的東西。
也許你知道線程池有三種 submit 方法,可是也許你根本不知道里面的任務分爲兩種類型,你就只知道往線程池裏面扔,也無論扔的是什麼類型的任務。
咱們先看一下 Callable 類型的任務是怎麼執行的:
public class JDKThreadPoolExecutorTest {
這裏利用 lambda 表達式,直接在任務體裏面帶上一個返回值,這時你看調用的方法就變成了這個:
運行結果也能拿到任務體裏面的返回了。輸出結果以下:
好,接下來再說說 submit 的任務爲 Runable 類型的狀況。
這個時候有兩個重載的形式:
標號爲 ① 的方法扔進去一個 Runable 的任務,返回一個 Future,而這個返回的 Future ,至關因而返回了一個寂寞。下面我會說到緣由。
標號爲 ② 的方法扔進去一個 Runable 的任務的同時,再扔進去一個泛型 T ,而巧好返回的 Future 裏面的泛型也是 T,那麼咱們大膽的猜想一下這就是同一個對象。若是是同一個對象,說明咱們能夠一個對象傳到任務體裏面去一頓操做,而後經過 Future 再次拿到這個對象的。一會就去驗證。
來,先驗證標號爲 ① 的方法,我爲啥說它返回了一個寂寞。
首先,仍是先把測試案例放在這裏:
public class JDKThreadPoolExecutorTest {
能夠看到,確實是調用的標號爲 ① 的方法:
同時,咱們也能夠看到 future.get() 方法的返回值爲 null。
你說,這不是返回了一個寂寞是幹啥?
當你想用標號爲 ① 的方法時,我勸你直接用 execute 方式提交任務。還不須要構建一個寂寞的返回值,徒增無用對象。
接下來,咱們看看標號爲 ② 的方法是怎麼用的:
public class JDKThreadPoolExecutorTest {
能夠看到改造以後,確實是調用了標號爲 ② 的方法:
future.get() 方法的輸出值也是異步任務中咱們通過計算後得出的 5201314。
你看,渣男就是這樣,明明不懂你,還非得用甜言蜜語來轟炸你。呸。
好了。綜上,線程池的提交方式一共有四種:一種 execute,無返回值。三種 submit,有返回值。
submit 中按照提交任務的類型又分爲兩種:一個是 Callable,一個是 Runable。
submit 中 Runable 的任務類型又有兩個重載方法:一個返回了個寂寞,一個返回了個渣男。哦,不。一個返回了個寂寞,一個返回了個對象。
這個時候就有人要站出來講:你說的不對,你就是瞎說,明明就只有 execute 這一種提交方式。
是的,「只有 execute 這一種提交方式」這一種說法也是沒錯的。
請看源碼:
三種 submit 方法裏面調用的都是 execute 方法。
能把前面這些方法娓娓道來,從表面談到內在的這種人,纔是好人。
只有愛你,纔會把你研究透。
固然,還有這幾種提交方式,用的很少,就不展開說了:
寫到這裏我不由想起了個人第三篇文章,真是奇怪的時間線開始收縮了的感受,《有的線程它死了,因而它變成一道面試題》,這篇文章裏面聊到了不一樣提交方式,對於異常的不一樣處理方式。
我就問你:一個線程池中的線程異常了,那麼線程池會怎麼處理這個線程?
你要是不知道,能夠去看看這篇文章,畢竟,有可能在面試的時候遇到的:
好,上面這些東西捋清楚了以後。咱們再聚焦到返回值 Future 上:
從上面的代碼咱們能夠看出,當咱們想要返回值的時候,都須要調用下面的這個 get() 方法:
而從這個方法的描述能夠看出,這是一個阻塞方法。拿不到值就在那裏等着。固然,還有一個帶超時時間的 get 方法,等指定時間後就不等了。
呸,渣男。沒耐心,這點時間都捨不得等。
總之就是有可能要等的。只要等,那麼就是阻塞。只要是阻塞,就是一個假異步。
因此總結一下這種場景下返回的 Future 的不足之處:
寫到這裏的時候我不由想起一個形象的例子,我給你舉一個。
假設你想約你的女神一塊兒去吃飯。女神嘛,確定是要先畫個美美的妝纔會出去逛街的。而女神化妝就能夠類比爲咱們提交的一個異步任務。
假設你是一個小屌絲,那麼女神就會對你說:我已經開始化妝了,你到樓下了就給我打電話。
而後你就收拾行頭準備出發,這就是你提交異步任務後還能夠作一些本身的事情。
你花了一小時到了女神樓下,打電話給她:女神你好,我到你樓下了。
女神說:你先等着吧,個人妝還沒畫好呢。
因而你開始等待,無盡的等待。這就是不帶超時時間的 future.get() 方法。
也有可能你硬氣一點,對女神說:我最多再等 24 小時哈,超過 24 小時不下樓,我就走了。
這就是帶超時時間的 future.get(timeout,unit) 方法:
結果 24 小時以後,女神還沒下來,你就走了。
固然,還有一種狀況就是你到樓下給女神打電話,女神說:哎,今天我男神約我出去看電影,就不和你去吃飯了哈。原本我想提早給你說的,可是我又記不起你電話,只有你打過來我才能告訴你。就這樣,你本身玩去吧。
這就至關於異步任務執行過程當中拋出了異常,而你只有在調用了 get 方法(打電話操做)以後才知道原來異常了。
而真正的異步是你不用等我,我好了我就叫你。
就像女神接到男神的電話時說的:我須要一點時間準備一下,你先玩本身的吧,我一會好了給你打電話。
這讓我想起了好萊塢原則:Don't Call Us,We'll Call you!
接下來,讓咱們見識一下真正的異步。
什麼叫真正的:「你先玩本身的,我一會好了叫你。」
女神說的:「好了叫你」。
就是一種回調機制。說到回調,那麼咱們就須要在異步任務提交以後,註冊一個回調函數就行。
Google 提供的 Guava 包裏面對 JDK 的 Future 進行了擴展:
新增了一個 addListenter 方法,入參是一個 Runnable 的任務類型和一個線程池。
使用方法,先看代碼:
public class JDKThreadPoolExecutorTest {
首先建立線程池的方式變了,須要用 Guava 裏面的 MoreExecutors 方法裝飾一下:
ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
而後用裝飾後的 executor 調用 submit 方法(任意一種),就會返回 ListenableFuture ,拿到這個 ListenableFuture 以後,咱們就能夠在上面註冊監聽:
因此,上面的程序咱們調用的是入參爲 callable 類型的接口:
從運行結果能夠看出來:獲取運行結果是在另外的線程裏面執行的,徹底沒有阻塞主線程。
和以前的「假異步」仍是有很大區別的。
除了上面的 addListener 方法外,其實我更喜歡用 FutureCallback 的方式。
能夠看一下代碼,很是的直觀:
public class JDKThreadPoolExecutorTest {
有 onSuccess 方法和 onFailure 方法。
上面的程序輸出結果爲:
若是異步任務執行的時候拋出了異常,好比女神被她的男神約走了,異步任務改爲這樣:
ListenableFuture<String> listenableFuture = executor.submit(() -> {
最終的運行結果就是這樣:
是的,女神去看電影了。她必定只是不想吃飯而已。
第一小節講的 Future 是 JDK 1.5 時代的產物:
通過了這麼多年的發展,Doug Lea 在 JDK 1.8 裏面引入了新的 CompletableFuture :
到了 JDK 1.8 時代,這纔是真正的異步編程。
CompletableFuture 實現了兩個接口,一個是咱們熟悉的 Future ,一個是 CompletionStage。
CompletionStage接口,你看這個接口的名稱中有一個 Stage :
能夠把這個接口理解爲一個任務的某個階段。因此多個 CompletionStage 連接在一塊兒就是一個任務鏈。前一個任務完成後,下一個任務就會自動觸發。
CompletableFuture 裏面的方法很是的多。
因爲篇幅緣由,我就只演示一個方法:
public class JDKThreadPoolExecutorTest {
該方法的執行結果以下:
咱們執行的時候並無指定用什麼線程池,可是從結果能夠看到也是異步的執行。
從輸出日誌中是能夠看出端倪的,ForkJoinPool.commonPool() 是其默認使用的線程池。
固然,咱們也能夠本身指定。
這個方法在不少開源框架裏面使用的仍是很是的多的。
接下來主要看看 CompletableFuture 對於異常的處理。我以爲很是的優雅。
不須要 try-catch 代碼塊包裹,也不須要調用 Future.get() 才知道異常了,它提供了一個 handle 方法,能夠處理上游異步任務中出現的異常:
public class JDKThreadPoolExecutorTest {
因爲女神在化妝的時候,接到男神的電話約她看電影,就只能放你鴿子了。
因此,上面程序的輸出結果以下:
若是,你順利把女神約出來了,是這樣的:
好了,女神都約出來了,文章就到這裏了。去幹正事吧。
按照個人經驗,女神約出來了你須要準備好回答一個問題:
你看我今天有什麼不一樣?
首先這題就是一道送命題,回答到她預期的答案的機率很是的低。有可能她今天不同的地方就是換了一個指甲油、換了一個美瞳、換了一個耳環之類的。
很明顯,這些很是細節的地方咱們很難發現。可是別慫。
先含情脈脈的認真的盯着她,花一分鐘找答案,一分鐘後沒有找到答案,就說:
你天天都不同,天天都比昨天更加美麗。
好了,才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,還請你在後臺留言指出來,我對其加以修改。
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。
我是 why,一個被代碼耽誤的文學創做者,不是大佬,可是喜歡分享,是一個又暖又有料的四川好男人。
還有,重要的事情說三遍: 歡迎關注我呀。 歡迎關注我呀。 歡迎關注我呀。