JAVA語言異步非阻塞設計模式(原理篇)

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本系列文章共2篇,對 Java 語言的異步非阻塞模式進行科普。","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"《原理篇》","attrs":{}},{"type":"text","text":"講解異步非阻塞模型的原理,以及核心設計模式“Promise”的基本特性。","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"《應用篇》","attrs":{}},{"type":"text","text":"會展示更加豐富的應用場景,介紹 Promise 的變體,如異常處理、調度策略等,並將 Promise 和現有工具進行對比。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"限於個人水平和篇幅,本系列以科普爲主,內容更偏重於原理、API 設計、應用實踐,但是不會深入講解併發優化的具體細節。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1.概述","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"異步非阻塞[A]是一種高性能的線程模型,在 IO 密集型系統中得到廣泛應用。在該模型下,系統發起耗時請求後不需要等待響應,期間可以執行其他操作;當收到響應後,系統收到通知並執行後續處理。由於消除了不必要的等待,這種模型能夠充分利用 cpu、線程等資源,提高資源利用率。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然而,異步非阻塞模式在提升性能的同時,也帶來了編碼實現上的複雜性。請求和響應可能分離到不同線程中,需要編寫額外代碼完成響應結果的傳遞。Promise 設計模式可以降低這種複雜性,封裝數據傳遞、時序控制、線程安全等實現細節,從而提供簡潔的 API 形式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文首先介紹異步非阻塞模式,從線程模型的角度分析阻塞和非阻塞模式的區別。之後介紹 Promise 設計模式的應用場景及工作流程。最後,提供一種簡易的 Java 實 現,能夠實現基本的功能需求,並做到線程安全。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在正式探索技術問題之前,我們先來看看什麼是","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"異步非阻塞模型","attrs":{}},{"type":"text","text":"。如圖1-1所示,展示了兩個小人通信的場景:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"兩個小人代表互相通信的兩個","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"線程","attrs":{}},{"type":"text","text":",如數據庫的客戶端和服務端;他們可以部署在不同的機器上。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"小人之間互相投遞蘋果,代表要","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"傳遞的消息","attrs":{}},{"type":"text","text":"。根據具體業務場景,這些消息可能會稱爲 request、response、packet、document、record 等。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"小人之間需要","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"建立信道","attrs":{}},{"type":"text","text":",消息才得以傳遞。根據場景,信道稱爲 channel、connection 等。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設左側小人發起請求,而右側小人處理請求併發送響應:左側小人先投出一個蘋果 request,被右側小人接收到;右側小人進行處理後,再投出蘋果 response,被左側小人接收到。我們考察左側小人在等待響應期間的行爲,根據他在等待 response 期間是否能處理其他工作,將其歸納爲“同步阻塞”和“異步非阻塞”兩種模式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/43/43ee61347282c236a6a5663f6fe3feb6.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖1-1 兩個小人通信","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們看看同步阻塞式通信的流程,如圖1-2a所示。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"投遞","attrs":{}},{"type":"text","text":"。左側小人投遞 request,並等待接收 response。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"等待","attrs":{}},{"type":"text","text":"。在等待接收 response 期間,左側小人休息。不論是否還有其他 request需要投遞、是否還有其他工作需要處理,他都視若無睹,絕對不會因此打斷休息。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"響應","attrs":{}},{"type":"text","text":"。在收到 response 後,小人從休息中喚醒並處理 response。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/1a/1a719778f4d1db8f719a7b0ac9536ffe.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖1-2a 同步阻塞式通信","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們看看異步非阻塞式通信的流程,如圖1-2b所示。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"緩存","attrs":{}},{"type":"text","text":"。左側小人投遞 request,並等待接收 response。和同步阻塞模式不同,小人並不需要親手接住蘋果 response,而是在地上放置一個盤子稱爲“buffer”;如果小人暫時不在場,那麼所收到的蘋果可以先存在盤子裏,稍後再處理。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"暫離","attrs":{}},{"type":"text","text":"。由於有盤子 buffer 的存在,小人投遞 request 後就可以暫時離開,去處理其他工作,當然也可以去投遞下一個 request;如果需要向不同的channel投遞request,那麼小人可以多擺放幾個盤子,和 channel 一一對應。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"響應","attrs":{}},{"type":"text","text":"。小人離開後,一旦某個盤子收到了 response,一隻“大喇叭”就會響起,發出“channelRead”通知,呼喚小人回來處理 response。如果要處理多個response 或多個 channel,那麼 channelRead 通知還需要攜帶參數,以說明從哪個 channel 上收到了哪個 response。","attrs":{}}]}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏的大喇叭可以用NIO或AIO來實現。簡單來說,NIO是指不停地輪詢每個盤子,一旦看到蘋果就發出通知;AIO是指在收到蘋果時直接觸發通知,而沒有輪詢的過程。當然,本系列文章的讀者並不需要了解更多實現細節,只需知道異步非阻塞模式依賴於“大喇叭”來實現,它替代小人等待接收 response,從而解放小人去處理其他工作。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/73/73e6a97ea28919486c7088a70543b6b9.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖1-2b 異步非阻塞式通信","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據上面的分析,同步模式具有下列嚴重","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"缺點","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"同步阻塞模式的工作效率十分低下","attrs":{}},{"type":"text","text":"。小人大部分時間都在休息,僅當投遞請求、處理響應時,才偶爾醒來工作一小會;而在異步非阻塞模式下,小人從不休息,馬不停蹄地投遞請求、處理響應,或處理其他工作。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"同步阻塞模式會帶來延遲","attrs":{}},{"type":"text","text":"。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們考慮下面兩種情況,如圖1-3所示。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"channel 複用","attrs":{}},{"type":"text","text":",即左側小人在一個 channel 上連續發送多條消息。在同步阻塞模式下,一輪(即請求+響應)只能投遞一個請求(蘋果1),而後續請求(蘋果2-4)都只能排隊等待,右側小人需要等待很多輪才能收到所期望的全部消息。此外,左側小人在等待接收某個 response 期間,沒有機會處理收到的其他消息,造成了數據處理的延遲。不得不感慨,左側小人太懶惰了!","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"線程複用","attrs":{}},{"type":"text","text":",即一個線程(小人)向多個 channel 發送消息(蘋果1-3,分別發向不同 channel)。左側小人同一時刻只能做一件事,要麼在工作,要麼在休息;他投遞了蘋果1後就躺下休息,等待響應,全然不顧右側小人2、3還在等待他們想要的蘋果2、3。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f7/f734eda48a44b0b87efe80bd1322526d.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖1-3a channel複用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/22/223b5966053fb2974db56c5aebb06221.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖1-3b 線程複用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這一章裏我們用漫畫的形式,初步體驗了同步阻塞模式與異步非阻塞模式,並分析了兩種模式的區別。接下來我們從Java線程入手,對兩種模式進行更加正式、更加貼近實際的分析。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2.異步非阻塞模型","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1 Java 線程狀態","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Java 程序中,線程是調度執行的單元。線程可以獲得 CPU 使用權來執行代碼,從而完成有意義的工作。工作進行期間,有時會因爲等待獲取鎖、等待網絡 IO 等原因而暫停,通稱“同步”或“阻塞”;如果多項工作能夠同時進行,之間不存在約束、不需要互相等待,這種情況就稱爲“異步”或“非阻塞”。受限於內存、系統線程數、上下文切換開銷,Java 程序並不能無限創建線程;因此,我們只能創建有限個線程,並儘量提高線程的利用率,即增加其工作時長、降低阻塞時長。異步非阻塞模型是減少阻塞、提高線程利用率的有效手段。當然,這種模型並不能消除所有的阻塞。我們首先來看看 Java 線程有哪些狀態,其中哪些阻塞是必要的,哪些阻塞可以避免。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Java 線程狀態包括:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"RUNNABLE","attrs":{}},{"type":"text","text":":線程在執行有意義的工作如圖2-1a,線程如果在執行純內存運算,那麼處於RUNNABLE狀態根據是否獲得cpu使用權,又分爲兩個子狀態:READY、RUNNING","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"BLOCKED/WAITING/TIMED_WAITING","attrs":{}},{"type":"text","text":":線程正在阻塞如圖2-1b、2-1c、2-1d,根據阻塞原因,線程處於下列狀態之一BLOCKED:synchronized等待獲取鎖WAITING/TIMED_WAITING:Lock等待獲取鎖。兩種狀態的區別爲是否設置超時時長","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5d/5d5f277eccae7b4af4c60479274f92cf.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖2-1 Java 線程狀態","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此外,如果 Java 線程正在進行網絡 IO,則線程狀態爲 RUNNABLE,但是實際上也發生了阻塞。以 socket 編程爲例,如圖2-2所示,在收到數據之前InputStream.read() 會阻塞,此時線程狀態爲RUNNABLE。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f6/f679758be2275801be66d38980d89d59.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖2-2 網絡IO","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"綜上,Java 線程狀態包括:RUNNABLE、BLOCKED、WAITING、TIMED_WAITING。其中,RUNNABLE 狀態又分爲內存計算(非阻塞)、網絡IO(阻塞)兩種情況,而其餘狀態都是阻塞的。根據阻塞原因,本文將 Java 線程狀態歸納爲以下3類:RUNNABLE、IO、BLOCKED","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"RUNNABLE","attrs":{}},{"type":"text","text":":Java 線程狀態爲 RUNNABLE,並且在執行有用的內存計算,無阻塞","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"IO","attrs":{}},{"type":"text","text":":Java線程狀態爲RUNNABLE,但是正在進行網絡IO,發生阻塞","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"BLOCKED","attrs":{}},{"type":"text","text":":Java線程狀態爲BLOCKED/WAITING/TIMED_WAITING,在併發工具的控制下,線程等待獲取某一種鎖,發生阻塞","attrs":{}}]}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要提高線程利用率,就要增加線程處於 RUNNABLE 狀態的時長,降低處於 IO 和BLOCKED狀態的時長。BLOCKED 狀態一般是不可避免的,因爲線程間需要通信,需要對臨界區進行併發控制;但是,如果採用適當的線程模型,那麼 IO 狀態的時長就可以得到降低,而這就是異步非阻塞模型。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.2 線程模型:阻塞 vs 非阻塞","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"異步非阻塞模型能夠降低 IO 阻塞時長,提高線程利用率。下面以數據庫訪問爲例,分析同步和異步 API 的線程模型。如圖3所示,過程中涉及3個函數:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"writeSync()或writeAsync():數據庫訪問,發送請求","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"process(result):處理服務器響應(以result表示)","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"doOtherThings():任意其他操作,邏輯上不依賴服務器響應","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"同步 API","attrs":{}},{"type":"text","text":" 如圖3-a 所示:調用者首先發送請求,然後在網絡連接上等待來自服務器的響應數據。API 會一直阻塞,直至收到響應才返回;期間調用者線程無法執行其他操作,即使該操作並不依賴服務器響應。實際的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"執行順序","attrs":{}},{"type":"text","text":"爲:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"writeSync()","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"process(result)","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"doOtherThings() // 直至收到結果,當前線程才能執行其他操作","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"異步 API","attrs":{}},{"type":"text","text":" 如圖2-3b所示:調用者發送請求並註冊回調,然後API立刻返回,接下來調用者可以執行任意操作。稍後底層網絡連接收到響應數據,觸發調用者所註冊的回調。實際的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"執行順序","attrs":{}},{"type":"text","text":"爲:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"writeAsync()","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"doOtherThings() // 已經可以執行其他操作,並不需要等待響應","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"process(result)","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ce/ceffffc3d4c1f8b670eb9ec28573ff1b.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖2-3 同步API & 異步API","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上述過程中,函數 doOtherThings() 並不依賴服務器響應,原則上可以和數據庫訪問同時執行。然而對於同步 API,調用者被迫等待服務器響應,然後纔可以執行 doOtherThings();即數據庫訪問期間線程阻塞於 IO 狀態,無法執行其他有用的操作,利用率十分低下。而異步 API 就沒有這個限制,顯得更加緊湊、高效。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 IO 密集型系統中,適當使用異步非阻塞模型,可以提升數據庫訪問吞吐量。考慮這樣一個場景:需要執行多條數據庫訪問請求,且請求之間互相獨立,無依賴關係。使用同步 API 和異步 API,線程狀態隨時間變化的過程如圖2-4所示。線程交替處於 RUNNABLE 和 IO 狀態。在 RUNNABLE 狀態下,線程執行內存計算,如提交請求、處理響應。在 IO 狀態下,線程在網絡連接上等待響應數據。在實際系統中,內存計算的速度非常快,RUNNABLE 狀態的時長基本可忽略;而網絡傳輸的耗時會相對更長(幾十到幾百毫秒),IO 狀態的時長更加可觀。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"a.","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"同步API","attrs":{}},{"type":"text","text":":調用者線程一次只能提交一個請求;直到請求返回後,才能再提交下一個請求。線程利用率很低,大部分時間消耗在 IO 狀態上。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"b.","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"異步API","attrs":{}},{"type":"text","text":":調用者線程可以連續提交多個請求,而之前提交的請求都還沒有收到響應。調用者線程會註冊一些回調,這些回調存儲在內存中;稍後網絡連接上收到響應數據,某個接收線程被通知處理響應數據,從內存中取出所註冊的回調,並觸發回調。這種模型下,請求可以連續地提交、連續的響應,從而節約IO狀態的耗時。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b7/b7bbc09f29a46323780ff3fa7fdcbb0b.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖2-4 線程時間線:數據庫訪問","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"異步非阻塞模式在IO密集型系統中應用非常廣泛。常用的中間件,如http請求[D]、redis[E]、mongo DB[F]、elasticsearch[G]、influx DB[H],都支持異步 API。各位讀者可以在參考文獻中,查閱這些異步 API的樣例代碼。關於中間件的異步API,下面有幾個注意事項:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"redis 的常見客戶端有 jedis和lettuce [E]。其中lettuce提供了異步API,而jedis只能提供同步 API;二者對比參見文章[I]。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"kafka producer[J]的send()方法也支持異步API,但是該API實際上不是純異步的[K]:當底層緩存滿,或者無法獲取服務器(broker)信息時,send()方法會發生阻塞。個人認爲這是一個非常嚴重的設計缺陷。kafka常用於低延遲日誌採集場景,系統會將日誌通過網絡寫入到kafka服務器,以減少線程內的阻塞,提升線程吞吐量;稍後其他進程會從kafka消費所寫入的日誌,進行持久存儲。設想一個實時通信系統,單條線程每秒需要處理幾萬到幾十萬條消息,響應時間一般爲幾毫秒到幾十毫秒。系統在處理期間需要經常調用 send() 來上報日誌,如果每次調用都發生哪怕1秒的延遲(實際有可能達幾十秒),延遲積累起來也會嚴重劣化吞吐量和延遲。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,異步 API 有多種實現,包括線程池、select(如netty 4.x[L])、epoll等。其共同點是調用者不需要在某一條網絡連接上阻塞,以等待接收數據;相反,API底層常駐有限數目的線程,當收到數據後,某一線程得到通知並觸發回調。這種模型也稱爲“響應式”模型,非常貼切。限於篇幅原因,本文主要關注","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"異步 API 設計","attrs":{}},{"type":"text","text":",而不深入講解異步 API 的實現原理。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3.Promise設計模式","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1 API形式:同步、異步 listener、異步 Promise","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一章介紹了異步非阻塞模式和異步 API 的函數形式。異步 API 具有以下特徵:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"在提交請求時註冊回調;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"提交請求後,函數立刻返回,不需要等待收到響應;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"收到響應後,觸發所註冊的回調;根據底層實現,可以利用有限數目的線程來接收響應數據,並在這些線程中執行回調。","attrs":{}}]}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在保留異步特性的基礎上,異步 API 的形式可以進一步優化。上一章圖2-3b展示了異步 API 的 listener 版本,特點是在提交請求時必須註冊恰好一個回調;因而在下列場景下,listener API 會難以滿足功能需求,需要調用者做進一步處理:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"多個對象都關注響應數據,即需要註冊多個回調;但是 listener 只支持註冊一個回調。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"需要將異步調用轉爲同步調用。例如某些框架(如spring)需要同步返回,或者我們希望主線程阻塞直至操作完成,然後主線程結束、進程退出;但是 listener 只支持純異步,調用者需要重複編寫異步轉同步的代碼。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了應對上述場景,我們可以使用 Promise 設計模式來重構異步 API,以支持多個回調和同步調用。下面對同步 API、異步listener API、異步Promise API 的函數形式進行對比,如圖3-1所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"a.同步","attrs":{}},{"type":"text","text":":調用 writeSync() 方法並阻塞;收到響應後函數停止阻塞,並返回響應數據","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"b.異步listener","attrs":{}},{"type":"text","text":":調用 writeAsync() 方法並註冊 listener,函數立刻返回;收到響應後,在其他線程觸發所註冊的 listener;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"c.異步Promise","attrs":{}},{"type":"text","text":":調用 writeAsync(),但不需要在函數中註冊 listener,函數立刻返回 Promise 對象。調用者可以調用異步的Promise.await(listener),註冊任意數目的 listener,收到響應後會按順序觸發;此外,也可以調用同步的 Promise.await(),阻塞直至收到響應。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/63/630f9e8b03ec0556581255bf03dd14f3.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖3-1 API 形式:同步、異步listener、異步Promise","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"綜上,Promise API 在保持異步特性的前提下,提供了更高的靈活性。調用者可以自由選擇函數是否阻塞,以及註冊任意數目的回調。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3.2 Promise的特性與實現","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一節介紹了 Promise API 的使用樣例,其核心是一個 Promise 對象,支持註冊 listener,以及同步獲取響應 result;而本節將對 Promise 的功能進行更加詳細的定義。注意,本節並不限定 Promise 的某一具體實現(例:jdk CompletableFuture、netty DefaultPromise),只展示共有的、必須具備的特性;缺少這些特性,Promise 將無法完成異步傳遞響應數據的工作。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.2.1 功能特性","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Promise的基本方法","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Promise的基本功能是傳遞響應數據,需要支持下列方法,如表3-1所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/db/db7e830ef1f37e2098c7440ab3676e59.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面以上一小節的數據庫訪問 API 爲例,演示 Promise 的工作流程,如圖3-2所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"a.調用者調用 writeAsync() API,提交數據庫訪問請求並獲取 Promise 對象;然後調用 Promise.await(listener),註冊對響應數據的 listener。Promise 對象也可以傳遞給程序中其他地方,使得關心響應數據的其他代碼,各自注冊更多listener。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"b.writeAsync()內部,創建 Promise 對象並和這次請求關聯起來,假設以requestId 標識。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"c.writeAsync()底層常駐有限數目的線程,用於發送請求和接收響應。以 netty爲例,當從網絡上收到響應據後,其中一個線程得到通知,執行 channelRead() 函數進行處理;函數取出響應數據和對應的 Promise 對象,並調用Promise.signalAll() 進行通知。注意這裏是僞代碼,和 netty 中回調函數的實際簽名略有區別。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/44/440f75621d33b42957ad90fc5adb8db5.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖3-2a 提交數據庫訪問請求","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/03/03439675cbe949d05343dd6fdc97702e.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖3-2b 創建Promise對象","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/9f/9fda30e2b1f935efc10b7f667357e88f.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖3-2c 通知Promise對象","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"- Promise的時序","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Promise 的方法需要保證以下時序。此處以“A對B可見”來描述時序,即:如果先執行操作A(註冊listener)就會產生某種永久效應(永久記錄這個listener),之後再執行操作B(通知result)就必須考慮到這種效應,執行相應的處理(觸發之前記錄的listener)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"await(listener)對signalAll(result)可見:註冊若干listener後,通知result時必須觸發每一個listener,不允許遺漏。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"signalAll(result)對await(listener)可見:通知result後,再註冊listener就會立刻觸發。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"首次signalAll(result)對後續signalAll(result)可見。首次通知result後,result即唯一確定,永不改變。之後再通知 result 就會忽略,不產生任何副作用。請求超時是該特性一種典型應用:在提交請求的同時創建一個定時任務;如果能在超時時長內正確收到響應數據,則通知 Promise 正常結束;否則定時任務超時,通知Promise 異常結束。不論上述事件哪個先發生,都保證只採納首次通知,使得請求結果唯一確定。","attrs":{}}]}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此外,某次 await(listener) 最好對後續 await(listener) 可見,以保證listener 嚴格按照註冊順序來觸發。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"- Promise 的非線程安全實現","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如不考慮線程安全,那麼下列代碼清單可以實現Promise的基本特性;線程安全的實現見下一小節。代碼清單依次展示了await(listener): void、signalAll(result)、await(): result的實現。這裏有幾個","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"注意事項","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"字段 listeners 存儲 await(listener) 所註冊的 listener","attrs":{}},{"type":"text","text":"。字段類型爲LinkedList,以存儲任意數目的 listener,同時維護 listener 的觸發順序。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"字段 isSignaled 記錄是否通知過 result","attrs":{}},{"type":"text","text":"。如果 isSignaled=true,則後續調用 await(listener) 時立刻觸發 listener,且後續調用 signalAll(result) 時直接忽略。此外,我們以 isSignaled=true 而不是 result=null 來判斷是否通知過 result,因爲某些情況下 null 本身也可以作爲響應數據。例如,我們以Promise表示數據庫寫入的結果,通知 null 表示寫入成功,通知Exception 對象(或某一子類)表示失敗原因。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"signalAll(T result)在末尾處調用 listeners.clear() 以釋放內存","attrs":{}},{"type":"text","text":",因爲listeners 已經觸發過,不再需要在內存中存儲。","attrs":{}}]}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"public class Promise {\n\n private boolean isSignaled = false;\n private T result;\n\n private final List> listeners = new LinkedList<>();\n\n public void await(Consumer listener) {\n if (isSignaled) {\n listener.accept(result);\n return;\n }\n\n listeners.add(listener);\n }\n\n public void signalAll(T result) {\n if (isSignaled) {\n return;\n }\n\n this.result = result;\n isSignaled = true;\n for (Consumer listener : listeners) {\n listener.accept(result);\n }\n listeners.clear();\n }\n\n public T await() {\n // 適當阻塞,直至signalAll()被調用;實際實現見3.3節\n return result;\n }\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.2.2 線程安全特性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一章3.2.1節講解了 Promise 的功能,並提供了非線程安全的實現。本節展示如何使用併發工具,實現線程安全的 Promise,如下所示。有下列幾個注意事項:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"線程安全。各個字段均被多個線程訪問,因此都屬於臨界區,需要使用適當的線程安全工具進行上鎖,如 synchronized、Lock。一種最簡單的實現,是將全部代碼納入臨界區內,進入方法時上鎖,離開方法時放鎖。注意在使用 return 進行提前返回時,不要忘記放鎖。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"在臨界區外觸發 listener,以減少在臨界區內停留的時長,並減少潛在的死鎖風險。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"同步 await()。可以使用任何一種同步等待的工具來實現,如 CountDownLatch、Condition。此處使用 Condition 實現,注意根據 java 語法,操作 Condition 時必須先獲取 Condition所關聯的鎖。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"public class Promise {\n\n private final ReentrantLock lock = new ReentrantLock();\n private final Condition resultCondition = lock.newCondition();\n\n private boolean isSignaled = false;\n private T result;\n\n private final List> listeners = new LinkedList<>();\n\n public void await(Consumer listener) {\n lock.lock();\n if (isSignaled) {\n lock.unlock(); // 不要忘記放鎖\n listener.accept(result); // 在臨界區外觸發listener\n return;\n }\n\n listeners.add(listener);\n lock.unlock();\n }\n\n public void signalAll(T result) {\n lock.lock();\n if (isSignaled) {\n lock.unlock(); // 不要忘記放鎖\n return;\n }\n\n this.result = result;\n isSignaled = true;\n\n // this.listeners的副本\n List> listeners = new ArrayList<>(this.listeners);\n this.listeners.clear();\n lock.unlock();\n\n for (Consumer listener : listeners) {\n listener.accept(result); // 在臨界區外觸發listener\n }\n\n/* 操作Condition時須上鎖*/\n lock.lock();\n resultCondition.signalAll();\n lock.unlock();\n }\n\n public T await() {\n lock.lock();\n if (isSignaled) {\n lock.unlock(); // 不要忘記放鎖\n return result;\n }\n\n while (!isSignaled) {\n resultCondition.awaitUninterruptibly();\n }\n lock.unlock();\n\n return result;\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述實現僅做演示使用,仍有較大的改進空間。生產環境的實現原理,讀者可以參考jdk CompletableFutre、netty DefaultPromise。可以改進的地方包括:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"使用 CAS 設置響應數據","attrs":{}},{"type":"text","text":"。字段 isSignaled、result 可以合併爲一個數據對象,然後使用CAS進行設值,從而進一步降低阻塞時長。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"觸發listener的時序","attrs":{}},{"type":"text","text":"。在上述代碼中,Promise.signalAll() 會依次觸發listener;在此期間,如果其他線程調用了異步 await(listener),由於 Promise的響應數據已通知,該線程也會觸發 listener。上述過程中,兩個線程同時觸發listener,因此沒有嚴格保證觸發順序。作爲改進,類似於 netty DefaultPromise,Promise.signalAll() 內部可以設置一個循環,不斷觸發listener直至listeners排空,以防期間註冊了新的 listener;在此期間,新註冊的 listene r可以直接加入到 listeners 中,而不是立刻觸發。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"listener 的移除","attrs":{}},{"type":"text","text":"。在通知響應數據之前,Promise 長期持有 listener 的引用,導致 listener 對象無法被 gc。可以添加 remove(listener) 方法,或者允許僅持有 listener 的弱引用。","attrs":{}}]}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.2.3 須避免的特性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面的小節展示了 Promise 的特性與實現原理。純正的 Promise 是異步傳遞響應數據的工具,其應當只實現必要的數據傳遞特性,而不應當夾雜請求提交、數據處理等邏輯。接下來我們來看一看,Promise 在實現時應避免哪些特性,以防限制調用者所能做出的決策。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.異步 await() 發生阻塞;該規則不僅適用於 Promise,也適用於任何異步 API ·。異步API常用於實時通信等延時敏感的場景,作用是減少線程阻塞,避免推遲後續其他操作。一旦發生阻塞,系統的響應速度和吞吐量就會受到嚴重衝擊。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以連續提交數據庫請求爲例。如圖3-3a 所示,調用者調用了一個異步 API,連續提交3次寫入請求,並在所返回的 Promise 上註冊回調。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們考察 writeAsync()與await() 如發生阻塞阻塞,將會對調用者造成什麼影響,如圖3-3b所示。提交請求是純內存操作,線程處於 RUNNABLE 狀態;writeAsync() 或 await() 如果發生阻塞,則線程處於 BLOCKED 狀態,暫停工作而無法執行後續操作。當發生阻塞時,調用者每提交一個請求就不得不等待一段時間,從而降低了提交請求的頻率,進而推遲了服務器對這些請求的響應,使得系統的吞吐量降低、延遲上升。特別地,如果系統採用了多路複用機制,即一個線程可以處理多個網絡連接或多個請求,那麼線程阻塞將會嚴重拖慢後續請求的處理,造成比較難排查的故障。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常見的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"阻塞原因","attrs":{}},{"type":"text","text":"包括:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Thread.sleep()","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"向隊列提交任務,調用了BlockingQueue.put()和take();應改爲非阻塞的offer()和poll()","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"向線程池提交任務,ExecutorService.submit(),如果線程池拒絕策略爲CallerRunsPolicy,而任務本身又是耗時的。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調用了阻塞的函數,包括:InputStream.read()、同步的 Promise.await()、KafkaProducer.send()。注意 KafkaProducer.send() 雖然形式上是異步 API,但是在底層緩存滿或者無法獲取服務器(broker)信息時,send()方法仍會發生阻塞。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/31/31fcf46b6499d6c7ca7afa3c166e495c.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖3-3a 連續提交請求","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/51/51358062ff05e16925e52b444e4e3a6d.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖3-3b 請求處理時間線","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.綁定線程池(ExecutorService),用於執行請求。如圖3-4所示,線程池是異步API的一種可選模型,但並不是唯一實現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"線程池模型","attrs":{}},{"type":"text","text":"。爲了不阻塞調用者,API 內置了線程池來提交請求、處理響應;調用者可以向線程池連續提交多個請求,但是不需要等待響應。調用者提交一條請求後,線程池中的某條線程就會被獨佔,等待接收響應並進行處理,但在此之前無法再處理其他請求;完成處理後,該條線程重新變爲空閒,可以繼續處理後續請求。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"響應式模型","attrs":{}},{"type":"text","text":"。類似地,API 內置了發送和接收線程來提交請求、處理響應,調用者也不需要同步等待。調用者提交一條請求後,發送線程向網絡發送請求;完成發送後,線程立刻變爲空閒,可以發送後續請求。當收到響應數據時,接收線程得到通知以處理響應;完成處理後,線程立刻變爲空閒,可以處理後續響應數據。上述過程中,任何一條線程都不會被某一請求獨佔,即線程隨時都可以處理請求,而不需要等待之前的請求被響應。","attrs":{}}]}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"綜上,如果綁定了線程池,Promise 就實現了對其他模型(如響應式模型)的兼容性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/33/33a4f5d80a5c03c64cbc3264794d0f76.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"圖3-4 線程時間線:線程池 vs select","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.在構造方法創建 Promise 對象時,定義如何提交請求。這種方式只能定義如何處理單條請求,而無法實現請求的批量處理。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以數據庫訪問爲例,現代數據庫一般支持批量讀寫,以略微提升單次訪問的延遲爲代價,換來吞吐量顯著提升;如果吞吐量得到提升,那麼平均延遲反而會下降。下面的代碼片段展示了一個批量請求 API:數據對象 BulkRequest 可以攜帶多條普通請求 Request,從而實現批量提交。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/* 提交單條請求*/\nclient.submit(new Request(1));\nclient.submit(new Request(2));\nclient.submit(new Request(3));\n\n/* 提交批量請求*/\nclient.submit(new BulkRequest(\n new Request(1),\n new Request(2),\n new Request(3)\n));","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了充分利用“批量請求”的特性,調用者需要進行跨越多條請求的“宏觀調控”。請求產生後可以先緩存起來;等待一段時間後,取出所緩存的多條請求,組裝一個批量請求來一併提交。因此,如下面的代碼片段所示,在構造Promise時指定如何提交單條請求是沒有意義的,這部分代碼(client.submit(new Request(...)))並不會被執行;而實際希望執行的代碼,其實是提交批量請求(client.submit(new BulkRequest(...)))。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/* Promise:提交單條請求*/\nnew Promise<>(() -> client.submit(new Request(1)));\nnew Promise<>(() -> client.submit(new Request(2)));\nnew Promise<>(() -> client.submit(new Request(3)));","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":4,"normalizeStart":4},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"在構造方法創建 Promise 對象時,定義如何處理響應數據,而不允許後續對響應數據註冊回調。如下面的代碼片段所示,在構造 Promise 對象時,註冊了對響應數據的處理 process(result);但是除此以外,其他代碼也有可能關心響應數據,需要註冊回調 process1(result)、process2(result)。如果 Promise 只能在構造時註冊唯一回調,那麼其他關注者就無法註冊所需回調函數,即 Promise API 退化回 listener API。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/* 定義如何處理響應數據*/\nPromise promise = new Promise<>(result -> process(result));\n\n/* 其他代碼也關心響應數據*/\npromise.await(result -> process1(result));\npromise.await(result -> process2(result));","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"綜上,Promise 應該是一個純粹的數據對象,其職責是存儲回調函數、存儲響應數據;同時做好時序控制,保證觸發回調函數無遺漏、保證觸發順序。除此以外,Promise 不應該和任何實現策略相耦合,不應該雜糅提交請求、處理響應的邏輯。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"4.總結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文講解了異步非阻塞設計模式,並對同步 API、異步 listener API、異步 Promise API 進行了對比。相比於其他兩種 API,Promise API 具有無可比擬的靈活性,調用者可以自由決定同步返回還是異步返回,並允許對響應數據註冊多個回調函數。最後,本文講解了 Promise 基本功能的實現,並初步實現了線程安全特性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本系列共2篇文章,本文爲第1篇《原理篇》。在下一篇《應用篇》中,我們將看到 Promise 設計模式豐富的應用場景,將其和現有工具進行結合或對比,以及對 Promise API 進行進一步變形和封裝,提供異常處理、調度策略等特性。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"參考文獻","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"[A] 異步非阻塞IO","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://en.wikipedia.org/wiki/Asynchronous_I/O","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"[B] Promise","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://en.wikipedia.org/wiki/Futures_and_promises","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"[C] java線程狀態","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://segmentfault.com/a/1190000038392244","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"[D] http異步API樣例:apache HttpAsyncClient","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://hc.apache.org/httpcomponents-asyncclient-4.1.x/quickstart.html","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"[E] redis異步API樣例:lettuce","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://github.com/lettuce-io/lettuce-core/wiki/Asynchronous-API","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"[F] mongo DB異步API樣例:AsyncMongoClient","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://mongodb.github.io/mongo-java-driver/3.0/driver-async/getting-started/quick-tour/","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"[G] elasticsearch異步API樣例:RestHighLevelClient","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://www.elastic.co/guide/en/elasticsearch/client/java-rest/master/java-rest-high-document-index.html","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"[H] influx DB異步API樣例:influxdb-java","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://github.com/influxdata/influxdb-java/blob/master/MANUAL.md","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"[I] jedis vs lettuce","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://redislabs.com/blog/jedis-vs-lettuce-an-exploration/","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"[J] kafka","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"http://cloudurable.com/blog/kafka-tutorial-kafka-producer/index.html","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"[K] KafkaProducer.send()阻塞","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://stackoverflow.com/questions/57140680/kafka-asynchronous-send-not-really-asynchronous","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"[L] netty","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://netty.io/wiki/user-guide-for-4.x.html","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章