JAVA語言異步非阻塞設計模式(應用篇)

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/55/55c6af122f7b2ec148b24c32b3cc3360.gif","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":"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":"本系列文章共2篇。在上一篇《原理篇》中,我們看到了異步非阻塞模型,它能夠有效降低線程IO狀態的耗時,提升資源利用率和系統吞吐量。異步API可以表現爲listener或Promise形式;其中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的應用:","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":"第2章:Promise與線程池。","attrs":{}},{"type":"text","text":" 在異步執行耗時請求時,ExecutorService+Future是一個備選方案;但是相比於Future,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":"第3章:異常處理。","attrs":{}},{"type":"text","text":" Java程序並不總能成功執行請求,有時會遇到網絡問題等不可抗力。對於無法避免的異常情況,異步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":"第4章:請求調度。","attrs":{}},{"type":"text","text":" Java程序有時需要提交多條請求,這些請求之間可能存在一定的關聯關係,包括順序執行、並行執行、批量執行。異步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的具體實現,讀者在生產環境可以選擇一個Promise工具類(如netty DefaultPromise[A]、jdk CompletableFuture[B]等);此外,由於Promise的原理並不複雜,讀者也可以自行實現所需功能。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2.Promise與線程池","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Java程序有時需要執行耗時的IO操作,如數據庫訪問;在此期間,相比於純內存計算,IO操作的持續時間明顯更長。爲了減少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":"本章對幾種IO模型進行對比(見2.1節),考察調用者線程的阻塞情況。其中,Promise支持純異步的請求提交及響應數據處理,能夠最大程度地消除不必要的阻塞。在實際項目中,如果底層API不支持純異步,那麼我們也可以進行適當重構,使其和Promise兼容(見2.2節)。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1 對比:同步、Future、Promise","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本節對幾種 IO 模型進行對比,包括同步 IO、基於線程池(ExecutorService)的異步 IO、基於 Promise 的異步IO,考察調用者線程的阻塞情況。假設我們要執行數據庫訪問請求。由於需要跨越網絡,單條請求需要進行耗時的 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":"首先我們來看看","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":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"1.同步IO","attrs":{}},{"type":"text","text":"。db.writeSync()方法是同步阻塞的。函數阻塞,直至收到響應數據。因此,調用者一次只能提交一個請求,必須等待該請求返回,才能再提交下一個請求。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/* 提交請求並阻塞,直至收到響應數據*/\nString result = db.writeSync(\"data\");\nprocess(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","marks":[{"type":"strong","attrs":{}}],"text":"2.基於線程池(ExecutorService)的異步IO","attrs":{}},{"type":"text","text":"。db.writeSync()方法不變;但是將其提交到線程池中來執行,使得調用者線程不會阻塞,從而可以連續提交多條請求data1-3。","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":"提交請求後,線程池返回 Future 對象,調用者調用 Future.get() 以獲取響應數據。Future.get() 方法卻是阻塞的,因此調用者在獲得響應數據之前無法再提交後續請求。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/* 提交請求*/\n// executor: ExecutorService\nFuture resultFuture1 = executor.submit(() -> db.writeSync(\"data1\"));\nFuture resultFuture2 = executor.submit(() -> db.writeSync(\"data2\"));\nFuture resultFuture3 = executor.submit(() -> db.writeSync(\"data3\"));\n\n/* 獲取響應:同步*/\nString result1 = resultFuture1.get();\nString result2 = resultFuture2.get();\nString result3 = resultFuture3.get();\nprocess(result1);\nprocess(result2);\nprocess(result3);","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":"3.基於Promise的異步IO","attrs":{}},{"type":"text","text":"。db.writeAsync()方法是純異步的,提交請求後返回 Promise 對象;調用者調用Promise.await()註冊回調,當收到響應數據後觸發回調。","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":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/* 提交請求*/\nPromise resultPromise1 = db.writeAsync(\"data1\");\nPromise resultPromise2 = db.writeAsync(\"data2\");\nPromise resultPromise3 = db.writeAsync(\"data3\");\n\n/* 獲取響應:異步*/\nresultPromise1.await(result1 -> process(result1));\nresultPromise2.await(result2 -> process(result2));\nresultPromise3.await(result3 -> process(result3));","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所示。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"a.","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"同步 IO","attrs":{}},{"type":"text","text":"。調用者一次只能提交一個請求,在收到響應之前不能提交下一個請求。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"b.","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"基於線程池的異步 IO","attrs":{}},{"type":"text","text":"。同一組請求(請求1-3,以及請求4-6)可以連續提交,而不需要等待前一條請求返回。然而,一旦調用者使用 Future.get() 獲取響應數據(result1-3),就會阻塞而無法再提交下一組請求(請求4-6),直至實際收到響應數據。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"c.","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"基於 Promise 的異步 IO。","attrs":{}},{"type":"text","text":" 調用者隨時可以提交請求,並向Promise註冊對響應數據的回調函數;稍後接收線程向Promise通知響應數據,以觸發回調函數。上述過程中,調用者線程不需要等待響應數據,始終不會阻塞。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a4/a49ddbfa2a9bff6dfd83aa0ef0a2cc63.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-1a 線程時間線:同步IO","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/30/306334c7f3a16ff1a2d29dc7647d4664.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-1b 線程時間線:基於線程池的異步IO","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2d/2df938bc26820ca9480e364ac055ac01.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-1c 線程時間線:基於 Promise 的異步IO","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.2 Promise結合線程池","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"和 ExecutorService+Future相比,Promise 具有純異步的優點;然而在某些場景下也需要把 Promise 和線程池結合使用。例如:1.底層 API 只支持同步阻塞模型,不支持純異步;此時只能在線程池中調用 API,才能做到非阻塞。2.需要重構一段遺留代碼,將其線程模型從線程池模型改爲響應式模型;可以先將對外接口改爲 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":"下面的代碼片段展示了 ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Promise 和線程池結合的用法:","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":"創建 Promise 對象作爲返回值。注意這裏使用了 PromiseOrException,以防期間遇到異常;其可以通知響應數據,也可以在失敗時通知拋出的 Exception。詳見3.1小節。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"在線程池中執行請求(2a),並在收到響應數據後向 Promise 通知(2b)","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"處理線程池滿異常。線程池底層關聯一個 BlockingQueue 來存儲待執行的任務,一般設置爲有界隊列以防無限佔用內存,當隊列滿時會丟棄某個任務。爲了向調用者通知該異常,線程池的拒絕策略須設置爲 AbortPolicy,當隊列滿時丟棄所提交的任務,並拋出 RejectedExecutionException;一旦捕獲該異常,就要向 Promise 通知請求失敗。","attrs":{}}]}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"public PromiseOrException writeAsync() {\n// 1.創建Promise對象\n PromiseOrException resultPromise = new PromiseOrException<>();\n try {\n executor.execute(() -> {\n String result = db.writeSync(\"data\"); // 2a.執行請求。只支持同步阻塞\n resultPromise.signalAllWithResult(result); // 2b.通知Promise\n });\n\n }catch (RejectedExecutionException e){ // 3.異常:線程池滿\n resultPromise.signalAllWithException(e); \n }\n\n return resultPromise;\n}","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3.異常處理:PromiseOrException","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Java 程序有時會遇到不可避免的異常情況,如網絡連接斷開;因此,程序員需要設計適當的異常處理機制,以提升程序的容錯性。本章介紹異步 API 的異常處理,首先介紹 Java 語言異常處理規範;然後介紹 Promise 的變體 PromiseOrException,使得 Promise API 支持規範的異常處理。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1異常處理規範","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"個人認爲,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Java 代碼的異常處理","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","text":"顯式區分正常出口和異常出口。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"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","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"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":"異常是 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":"雖然 if-else 和異常都是控制流,但是程序員必","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"須辨析二者的使用場景","attrs":{}},{"type":"text","text":"。if-else 的各個分支一般是對等的,都用於處理正常情況;而函數的返回值和異常是不對等的,拋出異常表示函數遇到無法處理的故障,已經無法正常計算結果,其與函數正常工作所產生的返回值有本質區別。在 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":"以數據庫訪問爲例,下面的代碼對比了API進行異常處理的兩種形式。數據庫訪問過程中,如果網絡連接順暢,並且服務端能夠正確處理請求,那麼db.write()應該返回服務端的響應數據,如服務端爲所寫數據生成的自增id、條件更新實際影響的數據條數等;如果網絡連接斷開,或者客戶端和服務端版本不匹配導致請求無法解析,從而無法正常工作,那麼db.write()應該拋出異常以說明具體原因。從“是否正常工作”的角度看,上述兩種情況的性質是截然不同的,顯然應該選用異常作爲控制流,而不是 if-else。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/* 正確*/\ntry {\n String result = db.write(\"data\");\n process(result); // 正常出口\n} catch (Exception e) {\n log.error(\"write fails\", e); // 異常出口\n}\n\n/* 錯誤*/\nString resultOrError = db.write(\"data\");\nif (resultOrError.equals(\"OK\")) {\n process(resultOrError); // 正常出口\n} else {\n log.error(\"write fails, error: \" + resultOrError); // 異常出口\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","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"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":"Java 語言的異常處理體系中,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"異常主要分爲以下幾類","attrs":{}},{"type":"text","text":":Exception、RuntimeException、Error;三者都是Throwable 的子類,即可以被函數拋出。注意,由於 RuntimeException 是 Exception 的子類,本文爲避免混淆,“Exception”特指“是 Exception 但不是 RuntimeException ”的那些異常。","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":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"1. Exception","attrs":{}},{"type":"text","text":":程序外部的不可抗力造成的異常情況,如網絡連接斷開。即使 Java 代碼完美無瑕,也絕對不可能避免這類異常(拔掉網線試試!)。既然無法避免,這種異常就應當強制處理,以提升系統的容錯能力。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2. RuntimeException","attrs":{}},{"type":"text","text":":編程錯誤造成的異常情況,如數組下標越界ArrayOutOfBoundException、參數不符合取值範圍 IllegalArgumentException 等。如果程序員對 API 的輸入約束瞭如指掌,並在調用 API 之前對函數參數進行適當校驗,那麼 RuntimeException 是可以絕對避免的(除非被調 API 在應當拋 Exception 處,實際拋出了RuntimeException)。既然可以避免,這種異常就沒有必要強制處理。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然,人無完人。假設程序員真的違背了某些約束,函數拋出 RuntimeException 且未被處理,那麼作爲懲罰,線程或進程會退出,從而提醒程序員改正錯誤代碼。如果線程或進程必須常駐,就要對 RuntimeException 進行兜底,如下面的代碼所示。這裏將代碼缺陷視爲無法避免的異常情況,捕獲異常後可以記錄日誌、觸發告警,提醒稍後來修正缺陷。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"new Thread(()->{\n while (true){\n try{\n doSomething();\n }catch (RuntimeException e){ // 對RuntimeException進行兜底,以防線程中斷\n log.error(\"error occurs\", e);\n }\n }\n});","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"3. Error:jvm內部定義的異常","attrs":{}},{"type":"text","text":",如 OutOfMemoryError。業務邏輯一般不拋出 Error,而是拋出某種Exception或 RuntimeException。","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":"上述幾類異常中,只有 Exception 是強制處理的,稱爲 checked exception[C]。如下所示是一個 checked exception的例子。數據庫訪問DB.write()拋出Exception,表示遇到網絡斷開、消息解析失敗等不可抗情況。異常類型爲Exception而不是RuntimeException,以強制調用者添加catch子句處理上述情況;如調用者遺漏了 catch子句,則編譯器會報錯,從而提示調用者“這裏一定會遇到異常情況,必須進行處理”,以完善程序容錯能力。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 拋出異常,如果:\n * 1.網絡連接斷開\n * 2.消息無法解析\n * 3.業務邏輯相關,如服務端扣款時發現餘額不足\n * 4.…… // 任何無法避免的情況,都應該拋出Exception!\n */\npublic String write(Object data) throws Exception {\n return \"\";\n}\n\n/**\n * 處理異常\n */\ntry {\n String result = db.write(\"data\");\n process(result); \n} catch (Exception e) { // 如遺漏catch子句,則編譯不通過\n log.error(\"write fails, db: ..., data: ...\", e);\n}","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.2 Promise API的異常處理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"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":"顯式區分正常出口和異常出口;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不可抗的異常,要在編譯時刻強制處理。下面的代碼展示了 Promise API 要如何設計異常處理機制,以符合上述規範。","attrs":{}}]}]}],"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":"使用PromiseOrException來通知響應數據和異常","attrs":{}},{"type":"text","text":"。PromiseOrException是Promise的子類,泛型模版X爲數據對象ResultOrException,其含有2個字段result和e:e==null表示正常,此時字段result有效;e!=null表示異常,此時不要使用字段result。","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":"在“重載1”中,調用者從回調函數中獲得ResultOrException對象","attrs":{}},{"type":"text","text":"。調用ResultOrException.get()獲取響應數據result,或者get()方法拋出異常e。這種方式的代碼結構和傳統的異常處理一致,可以使用多個catch子句分別處理不同類型的異常。","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":"在“重載2”中,調用者從回調函數中直接獲得result和e","attrs":{}},{"type":"text","text":"。含義同上。這種方式省去了ResultOrException.get();但是如果需要處不同類型的異常,則需要用e instanceof MyException來判斷異常類型。","attrs":{}}]}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// extends Promise>\n PromiseOrException resultPromsie = db.writeAsync(\"data\");\n\n /* 重載1*/ \nresultPromsie.await(resultOrException -> { \n try {\n String result = resultOrException.get();\n process(result); // 正常出口\n } catch (Exception e) {\n log.error(\"write fails\", e); // 異常出口\n }\n});\n \n /* 重載2*/\n resultPromsie.await((result, e) -> {\n if (e == null) {\n process(result); // 正常出口\n } else {\n log.error(\"write fails\", e); // 異常出口\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":"PromiseOrException符合上一小節提出的異常處理規範,","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":"區分正常出口和異常出口。響應數據和異常分別使用 result 和e兩個變量來傳遞,可以靠e==null來判斷是否正常。注意result==null不能作爲判斷條件,因爲null有可能是響應數據的合法值。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"強制處理異常。不論使用哪一種回調,不存在一種代碼結構能夠只獲得 result 而不獲得e,因此語法上不會遺漏e的異常處理。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"允許定義異常類型。PromiseOrException 的泛型模版E填爲 Excetion 不是必需的,也可以填爲任意其他類型。注意,受限於 Java 語法,泛型模版處只允許填寫一種異常類型,而不像函數拋異常那樣允許拋出多種異常。爲應對這種限制,我們只能爲 API 定義一個異常父類,調用者用 catch 子句或 instanceof 進行向下轉型。當然,這種“定義異常父類”的做法也是可以接受的,並在現有工具中廣泛應用,因爲可以將工具所拋異常區別於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":":全部異常通過 PromiseOrException 來通知,而 API 本身不要拋出異常。以數據庫訪問 API writeAsync()爲例,面的代碼對比了兩種拋異常的方式。","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"正確的做法","attrs":{}},{"type":"text","text":"是PromiseOrException 作爲唯一出口,如果 API 底層實現拋出異常(submit() throws Exception),則應該將異常封裝於 PromiseOrException 對象,而不應該直接從API函數拋出(writeAsync() throws Exception)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/* 正確:唯一出口PromiseOrException*/\npublic PromiseOrException writeAsync(Object data) {\n try {\n submit(data); // throws exception\n } catch (Exception e) {\n return PromiseOrException.immediatelyException(e);\n }\n PromiseOrException resultPromise = new PromiseOrException<>();\n return resultPromise;\n}\n\n/* 錯誤:兩個出口throws Exception和PromiseOrException*/\npublic PromiseOrException writeAsync(Object data) throws Exception {\n submit(data); // throws exception\n\n PromiseOrException resultPromise = new PromiseOrException<>();\n return resultPromise;\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":"如果錯誤地設計了含有兩個異常出口的 API,調用者就不得不重複書寫異常處理邏輯,如下面的代碼所示。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"try {\n PromiseOrException resultPromise = db.writeAsync(\"data\");\n resultPromise.await((result, e) -> {\n if (e == null) {\n process(result); // 正常出口\n } else {\n log.error(\"write fails\", e); // 異常出口2\n }\n });\n} catch (Exception e) {\n log.error(\"write fails\", e); // 異常出口1\n}","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":"Java 程序中有時需要提交多條異步請求,且這些請求之間存在一定的關聯關係。在異步非阻塞場景下,這些關聯關係都可以藉助 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":"1.順序請求","attrs":{}},{"type":"text","text":",如圖4-1所示。後一條請求依賴前一條請求的響應數據;因此,必須等待前一條請求返回,才能構造並提交下一條請求。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2c/2c2c5cb4f4bdfc3dcc9b9e4a285a8e25.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":"圖4-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","marks":[{"type":"strong","attrs":{}}],"text":"2.並行請求","attrs":{}},{"type":"text","text":",如圖4-2所示。一次提交多條請求,然後等待全部請求返回。所提交的請求之間沒有依賴關係,因此可以同時執行;但是必須收到每條請求的響應數據(發生channelRead()事件,事件參數爲響應數據),才能執行實際的處理process(result1,2,3)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/01/0155cd28684a39bc1ad81b5fbdf9bda0.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":"圖4-2 並行請求","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":"3.批量請求","attrs":{}},{"type":"text","text":",如圖4-3所示。調用者連續提交多條請求,但是暫存在隊列中(offer()),而不是立刻執行。一段時間後,從隊列中取出若干條請求,組裝爲批量請求來提交(writeBulk());當收到批量請求的響應消息時,可以從中取出每條請求的響應數據。由於每次網絡IO都帶來額外開銷,故實際應用中經常使用批量請求來減少網絡IO頻率,以提升總體吞吐量。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d3/d3bd46c78eae3a616bd1165d06b7640f.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":"圖4-3 批量請求","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.1.順序請求:Promise.then()","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設一系列操作需要依次完成,即前一操作完成後,才能開始執行下一操作;如果這些操作均表現爲 Promise API,我們可以對 Promise.await(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。submit 方法提交請求 request 並返回 Promise 對象;當收到響應數據時, 該 Promise 對象被通知。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 異步Promise API\n */\n public static Promise submit(Object request) {\n Promise resultPromise = new Promise<>();\n // ……\n return resultPromise;\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":"現假設有5個請求稱爲“A”-“E”,這些請求必須依次提交。例如,由於請求B的參數依賴請求 A 的響應數據,故提交請求A後必須先處理其響應數據 resultA,然後才能再提交請求B。這種場景可以用如下所示的代碼來實現。某次調用submit(\"X\")函數後,我們在其返回的 Promise 對象上註冊回調;回調函數內處理響應數據 resultX,並調用submit(\"X+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":"這種方式雖然能實現功能需求,但是嵌套式的代碼結構可讀性非常差——每增加一個請求,代碼就要多嵌套、縮進一個層級。當調用邏輯複雜、請求數較多時,代碼會非常難以維護。","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":"[D],在 JavaScript 語言中相關討論頗多,可以作爲參考。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"submit(\"A\").await(resultA -> {\n submit(\"B\").await(resultB -> {\n submit(\"C\").await(resultC -> {\n submit(\"D\").await(resultD -> {\n submit(\"E\").await(resultE -> {\n process(resultE);\n });\n });\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":"爲改進代碼結構,我們對 Promise.await(Consumer) 方法進行封裝,提供Promise.then(Function>)方法,如下所示。類似於await(),then()也可以註冊一個回調函數resultX->submit(\"X+1\"),回調函數處理響應數據resultX,並提交下一請求submit(\"X+1\");then()的返回值即submit(\"X+1\")的返回值,用於通知下一請求的響應數據resultX+1。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"Promise resultPromiseA = submit(\"A\");\nPromise resultPromiseB = resultPromiseA.then(resultA -> submit(\"B\"));\nPromise resultPromiseC = resultPromiseB.then(resultB -> submit(\"C\"));\nPromise resultPromiseD = resultPromiseC.then(resultC -> submit(\"D\"));\nPromise resultPromiseE = resultPromiseD.then(resultD -> submit(\"E\"));\nresultPromiseE.await(resultE -> process(resultE));","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":"接下來,我們將中間變量 resultPromiseA-E 內聯,即得到基於then()的鏈式調用結構。相比於await(),then()消除了套娃般的嵌套回調。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"submit(\"A\")\n .then(resultA -> submit(\"B\")) // 返回resultPromiseB\n .then(resultB -> submit(\"C\")) // 返回resultPromiseC\n .then(resultC -> submit(\"D\")) // 返回resultPromiseD\n .then(resultD -> submit(\"E\")) // 返回resultPromiseE\n .await(resultE -> process(resultE));","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":"Promise.then() 的一種簡單實現","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":"then()方法提供一個泛型模版Next,以說明下一請求的響應數據類型。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"根據泛型模版Next,then()內部創建Promise作爲返回值,用於通知下一請求的響應數據。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"對於當前請求,調用await()註冊響應數據的回調result;當收到響應數據後,執行函數func,以提交下一請求:func.apply(result)。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"當收到下一請求的響應數據後,Promise被通知:nextPromise::signalAll。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"public Promise then(Function> func) {\n Promise nextPromise = new Promise<>();\n await(result -> {\n Promise resultPromiseNext = func.apply(result);\n resultPromiseNext.await(nextPromise::signalAll);\n });\n return nextPromise;\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.then(Function>)。根據回調函數是否有返回值、同步執行還是異步執行,Promise可以提供then()的更多種重載;受限於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":"thenRun(Runnable)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"thenAccept(Consumer)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"thenApply(Function)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"thenApplyAsync(Function>)","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.2.並行請求:LatchPromise","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一小節介紹了“順序請求”的場景,即多條請求需要依次執行;而“並行請求”場景下,多條請求之間沒有順序約束,但是我們仍然需要等待全部請求返回,才能執行後續操作。例如,我們需要查詢多張數據庫表,這些查詢語句可以同時執行;但是必須等待每條查詢都返回,我們才能獲得完整信息。jdk 提供CountDownLatch 來實現這一場景,但是其只支持同步等待;作爲改進,我們採用 LatchPromise 實現相同的功能,並且支持純異步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":"以數據庫訪問爲例,如下所示的代碼","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"展示了 LatchPromise 的使用","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":"提交3條請求,並獲取每個請求所對應的 Promise 對象 resultPromise1-3,以獲取響應數據。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"創建LatchPromise對象,並向其註冊需要等待的 Promise 對象resultPromise1-3。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"LatchPromise.untilAllSignaled()返回一個 Promise 對象 allSignaled。當所註冊的 resultPromise1-3均被通知後,allSignaled 會被通知。allSignaled 的類型爲 VoidPromise,表示 allSignaled 被通知時沒有需要處理的響應數據。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"在 allSignaled 上註冊回調,在回調函數中調用 resultPromiseX.await() 獲取實際的響應數據;此時由於請求已執行完畢,故 await() 立刻返回而不阻塞。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/* 創建Promise對象*/\nPromise resultPromise1 = db.writeAsync(\"a\");\nPromise resultPromise2 = db.writeAsync(\"b\");\nPromise resultPromise3 = db.writeAsync(\"c\");\n\n/* 向LatchPromise註冊要等待的Promise*/\nLatchPromise latch = new LatchPromise();\nlatch.add(resultPromise1);\nlatch.add(resultPromise2);\nlatch.add(resultPromise3);\n\n/* 等待全部Promise被通知*/\nVoidPromise allSignaled = latch.untilAllSignaled();\nallSignaled.await(() -> {\n String result1 = resultPromise1.await();\n String result2 = resultPromise2.await();\n String result3 = resultPromise3.await();\n process(result1, result2, result3);\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":"作爲對比,下面的代碼使用 CountDownLatch 實現相同功能,但是","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","text":"CountDownLatch.await() 只支持同步等待。在純異步場景下是無法接受的。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"CountDownLatch 對業務邏輯有侵入性。程序員需要在業務邏輯中添加對 CountDownLatch.countDown()的調用,以控制CountDownLatch 的時序;相反,LatchPromise 依賴本來就已經存在的 resultPromise 對象,而不需要編寫額外的時序控制代碼。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"CountDownLatch 引入了冗餘邏輯。創建 CountDownLatch 時,必須在構造參數中填寫要等待的請求數;因此,一旦所提交的請求的數目改變,就必須相應地更新創建 CountDownLatch 的代碼,修改構造參數。","attrs":{}}]}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"CountDownLatch latch = new CountDownLatch(3);\nresultPromise1.await(result1 -> latch.countDown());\nresultPromise2.await(result2 -> latch.countDown());\nresultPromise3.await(result3 -> latch.countDown());\n\nlatch.await();\nString result1 = resultPromise1.await();\nString result2 = resultPromise2.await();\nString result3 = resultPromise3.await();\nprocess(result1, result2, result3); ","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":"LatchPromise 的參考實現","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":"設立countUnfinished變量,記錄還沒有被通知的Promise對象的數目","attrs":{}},{"type":"text","text":"。每當註冊一個Promise對象,countUnfinished遞增;每當一個Promise被通知,countUnfinished遞減。當countUnfinished減到0時,說明所註冊全部Promise對象都被通知了,故通知allSignaled。","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":"設立noMore變量","attrs":{}},{"type":"text","text":",記錄是否還需要繼續註冊新的Promise對象,僅當調用了untilAllSignaled()才認爲完成註冊;在此之前,即使countUnfinished減至0,也不應該通知allSignaled。考慮這樣一種情況:需要註冊並等待resultPromise1-3,其中resultPromise1、2在註冊期間即已被通知,而resultPromise3未被通知。如果不判斷noMore,那麼註冊完resultPromise1、2後,countUnfinished即已減至0,導致提前通知allSignaled;這是一個時序錯誤,因爲實際上resultPromise3還沒有完成。","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":",此處使用synchronized來實現。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"注意","attrs":{}},{"type":"text","text":",調用untilAllSignaled()時,如果countUnfinished的初值已經爲0,則應立刻通知allSignaled;因爲countUnfinished已經不可能再遞減,之後沒有機會再通知allSignaled了。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// private static class Lock。無成員,僅用於synchronized(lock)\nprivate final Lock lock = new Lock();\nprivate int countUnfinished = 0;\nprivate final VoidPromise allSignaled = new VoidPromise();\n\npublic void add(Promise> promise) {\n if (promise.isSignaled()) {\n return;\n }\n\n synchronized (lock) {\n countUnfinished++;\n }\n\n promise.await(unused -> {\n synchronized (lock) {\n countUnfinished--;\n if (countUnfinished == 0 && noMore) {\n allSignaled.signalAll();\n }\n }\n });\n}\n\npublic VoidPromise untilAllSignaled() {\n synchronized (lock) {\n if (countUnfinished == 0) {\n allSignaled.signalAll();\n } else {\n noMore = true;\n }\n }\n\n return allSignaled;\n}","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"4.3.批量請求:ExecutorAsync","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"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":"“批量請求”(也稱“bulk”、“batch”)是指發送一條消息即可攜帶多條請求,主要用於數據庫訪問和遠程調用等場景。由於減少了網絡 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":"很多數據庫 API 都支持批量讀寫,如JDBC PreparedStatement[E]、elasticsearch bulk API[F]、mongo DB insertMany()[G]、influx DB BatchPoints[H],讀者可以查閱參考文獻進一步瞭解。爲了提升性能,部分API會犧牲易用性。其中,elasticsearch bulk API 對調用者的限制最少,允許混雜增刪改等不同類型的請求,允許寫入不同的數據庫表(index);mongo DB、influx DB 次之,一個批量請求只能寫入同一個數據庫表,但是可以自定義每條數據的字段;PreparedStatement 的靈活性最低,其定義了 SQL 語句的模版,調用者只能填寫模版參數,而不能修改語句結構。","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 已經支持批量訪問,但是很多原生 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":"在此,我們設計出通用組件 ExecutorAsync,封裝請求調度策略以提供更簡潔的API。","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"ExecutorAsync 的使用流程","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":"類似於線程池 ExecutorService.submit(),調用者可以調用ExecutorAsync.submit()來提交一個請求。其中,請求以數據對象 Request 表示,用於存儲請求類型和請求參數。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"提交請求後,調用者獲得 Promise 對象,以獲取響應數據。由於使用了 Promise,ExecutorAsync 支持純異步操作,提交請求和獲取響應數據都不需要阻塞。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"ExecutorAsync 內部對請求進行調度,並非提交一條請求就立刻執行,而是每隔固定時間收集一批請求,將其組裝爲一個批量請求,再調用實際的數據庫訪問 API。如果數據庫訪問 API 允許,那麼一批請求可以混雜不同的請求類型,或者操作不同的數據庫表。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"ExecutorAsync executor = new ExecutorAsync();\nPromise<...> resultPromise1 = executor.submit(new Request(\"data1\"));\nPromise<...> resultPromise2 = executor.submit(new Request(\"data2\"));\nPromise<...> resultPromise3 = executor.submit(new Request(\"data3\"));","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":"具體而言,ExecutorAsync 支持如下調度策略:","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":"1.排隊","attrs":{}},{"type":"text","text":",如圖4-4a所示。調用者提交請求Request後不要立刻執行,而是將其緩存在隊列queue中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c2/c275260ea662c626ef1d75971533626a.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":"圖4-4a ExecutorAsync特性:排隊","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":"2.批量","attrs":{}},{"type":"text","text":",如圖4-4b所示。每隔固定時間間隔,ExecutorAsync從隊列中取出若干條請求,將其組裝爲批量請求bulk,並調用底層數據庫API提交給服務端。如果隊列長度增長得很快,我們也可以定義一個批量大小bulk size,當隊列長度到達該值時立刻組裝一個批量請求並提交。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/aa/aaf8563140b3b3be19e41f70c877bb4f.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":"圖4-4b ExecutorAsync 特性:批量","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":"3.併發","attrs":{}},{"type":"text","text":",如圖4-4c所示。如果底層數據庫 API 支持異步提交請求,那麼 ExecutorAsync 就可以充分利用這種特性,連續提交多個批量請求,而不需要等待之前的批量請求返回。爲避免數據庫服務器超載,我們可以定義併發度 parallelism,限制正在執行(in flight)的批量請求的數目;當達到限制時,如果調用者再提交新的請求,就暫存在隊列 queue 中等待執行,而不會組裝新的批量請求。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/68/6821fb25a533eb75e1fc6c004768c0a1.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":"圖4-4c ExecutorAsync 特性:併發","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":"4.丟棄","attrs":{}},{"type":"text","text":"。如圖4-4d所示。在上文提到的 bulk size 和 parallelism 的限制下,如果提交請求的速率遠高於服務端響應的速率,那麼大量請求就會堆積在隊列中等待處理,最終導致超時失敗。在這種情況下,將請求發送給服務端已經沒有意義,因爲調用者已經認定請求失敗,而不再關心響應數據。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e0/e04761d4dd1d2ad408cb98228b85b9ba.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":"圖4-4d 請求超時","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":"因此,ExecutorAsync 應該及時從隊列中移除無效請求,而剩餘請求仍然“新鮮”。這種策略能夠強制縮短隊列長度,以降低後續請求在隊列中的堆積時長、預防請求超時;同時,由於避免存儲和發送無效請求,這種策略也能節約內存和 IO 開銷。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/17/17c008ce182273eae10ec0b90ebcf66e.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":"圖4-4e ExecutorAsync特性:丟棄","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":"italic","attrs":{}},{"type":"strong","attrs":{}}],"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":"上一小節我們看到了 ExecutorAsync 的調度策略,包括排隊、批量、併發、丟棄。如下面的代碼所示,ExecutorAsync 只需對外提供 submit(Request) 方法,用於提交單條請求。請求以數據對象 Request 表示,其字段 Request.resultPromise 是 Promise 對象,用於通知響應數據;在需要進行異常處理的場景下,我們使用 PromiseOrException作爲 Promise 的實現,其中泛型模版T改爲響應數據的實際類型。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"public class ExecutorAsync {\n\n public PromiseOrException submit(Request request) {\n return request.resultPromise;\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":"接下來我們來看看 ExecutorAsync 的實現原理。由於源碼細節較多、篇幅較長,故本節用流程圖的形式,來講解更高層的設計,如圖4-5所示。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cd/cd51a4373a87489cbbd5c2264d782528.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":"圖4-5 ExecutorAsync原理","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":"1.提交請求","attrs":{}},{"type":"text","text":"。調用者調用 ExecutorAsync.submit(Request),每次調用提交一條請求。該條請求存入隊列 queue,等待後續調度執行。參數 Request 的結構如下面的代碼所示,包括下列字段:","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":"predicate:函數,判斷請求是否有效,無效請求(如超時的請求)將被丟棄。詳見步驟2。","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":"resultPromise:通知響應數據。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"public class Request {\n public final PredicateE predicate; \n public final PromiseOrException resultPromise;\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","marks":[{"type":"strong","attrs":{}}],"text":"2.每隔固定間隔,或者queue.size()達到bulk size,嘗試組裝批量請求","attrs":{}},{"type":"text","text":"。從隊列queue中依次取出請求,每條請求執行函數 Request.predicate,以判斷是否仍然要提交該請求;取出的有效請求的條數,不超過bulk size。predicate 是一個函數,類似於 jdk Predicate 接口,形式如下面的代碼所示。接口函數test()可以正常返回,表示請求仍然有效;也可以拋出異常,說明請求無效的原因,如等待超時。如果拋出異常,則該條請求直接丟棄,並將發生的異常將通知給 Request.resultPromise,使得調用者執行異常處理邏輯。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"public interface PredicateE {\n void test() throws Exception;\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","marks":[{"type":"strong","attrs":{}}],"text":"3.提交批量請求","attrs":{}},{"type":"text","text":"。第2步從隊列 queue 中取出了至多 bulk size 條請求,將其作爲參數調用RequestFunc.execute(requests),以提交批量請求。接口 RequestFunc 的形式如下面的代碼所示。接口方法execute(requests)以若干條請求爲參數,將其組裝爲批量請求,調用底層的數據庫 API 來提交。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"public interface RequestFunc{\n void execute(List> requests);\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","marks":[{"type":"strong","attrs":{}}],"text":"4.當收到響應後,對於每條請求,依次向 Request.resultPromise 通知響應數據","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","marks":[{"type":"strong","attrs":{}}],"text":"5.爲防止服務端超載,ExecutorAsync 可限制併發請求數不超過 parallelism","attrs":{}},{"type":"text","text":"。我們設置計數變量 inFlight=0,以統計正在執行的批量請求數:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"a.當嘗試組裝批量請求(步驟2)時,首先判斷inFlight
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章