天天看點

JDK 僞異步程式設計(線程池)

BIO主要的問題在于每當有一個新的用戶端請求接入時,服務端必須建立一個新的線程處理新接入的用戶端鍊路,一個線程隻能處理一個用戶端連接配接。在高性能伺服器應用領域,往往需要面向成千上萬個用戶端的并發連接配接,這種模型顯然無法滿足高性能、高并發接入的場景。為了改進一線程一連接配接模型,後來又演進出了一種通過線程池或者消息隊列實作1個或者多個線程處理N個用戶端的模型,由于它的底層通信機制依然使用同步阻塞I/O,是以被稱為“僞異步”。

為了解決同步阻塞I/O面臨的一個鍊路需要一個線程處理的問題,後來有人對它的線程模型進行了優化,後端通過一個線程池來處理多個用戶端的請求接入,形成用戶端個數M:線程池最大線程數N的比例關系,其中M可以遠遠大于N,通過線程池可以靈活的調配線程資源,設定線程的最大值,防止由于海量并發接入導緻線程耗盡。

當有新的用戶端接入的時候,将用戶端的Socket封裝成一個Task(該任務實作java.lang.Runnable接口)投遞到後端的線程池中進行處理,JDK的線程池維護一個消息隊列和N個活躍線程對消息隊列中的任務進行處理。由于線程池可以設定消息隊列的大小和最大線程數,是以,它的資源占用是可控的,無論多少個用戶端并發通路,都不會導緻資源的耗盡和當機。

JDK 僞異步程式設計(線程池)

由于線程池和消息隊列都是有界的,是以,無論用戶端并發連接配接數多大,它都不會導緻線程個數過于膨脹或者記憶體溢出,相比于傳統的一連接配接一線程模型,是一種改良。用戶端代碼并沒有改變,詳見BIO的類。

當對Socket的輸入流進行讀取操作的時候,它會一直阻塞下去,直到發生如下三種事件。

有資料可讀;

可用資料已經讀取完畢;

發生空指針或者I/O異常。

這意味着當對方發送請求或者應答消息比較緩慢、或者網絡傳輸較慢時,讀取輸入流一方的通信線程将被長時間阻塞,如果對方要60s才能夠将資料發送完成,讀取一方的I/O線程也将會被同步阻塞60s,在此期間,其他接入消息隻能在消息隊列中排隊。

當調用OutputStream的write方法寫輸出流的時候,它将會被阻塞,直到所有要發送的位元組全部寫入完畢,或者發生異常。學習過TCP/IP相關知識的人都知道,當消息的接收方處理緩慢的時候,将不能及時地從TCP緩沖區讀取資料,這将會導緻發送方的TCP window size不斷減小,直到為0,雙方處于Keep-Alive狀态,消息發送方将不能再向TCP緩沖區寫入消息,這時如果采用的是同步阻塞I/O,write操作将會被無限期阻塞,直到TCP window size大于0或者發生I/O異常。

通過對輸入和輸出流的API文檔進行分析,我們了解到讀和寫操作都是同步阻塞的,阻塞的時間取決于對方I/O線程的處理速度和網絡I/O的傳輸速度。本質上來講,我們無法保證生産環境的網絡狀況和對端的應用程式能足夠快,如果我們的應用程式依賴對方的處理速度,它的可靠性就非常差。也許在實驗室進行的性能測試結果令人滿意,但是一旦上線運作,面對惡劣的網絡環境和良莠不齊的第三方系統,問題就會如火山一樣噴發。

僞異步I/O實際上僅僅隻是對之前I/O線程模型的一個簡單優化,它無法從根本上解決同步I/O導緻的通信線程阻塞問題。下面我們就簡單分析下如果通信對方傳回應答時間過長,會引起的級聯故障。

(1)服務端處理緩慢,傳回應答消息耗費60s,平時隻需要10ms。

(2)采用僞異步I/O的線程正在讀取故障服務節點的響應,由于讀取輸入流是阻塞的,是以,它将會被同步阻塞60s。

(3)假如所有的可用線程都被故障伺服器阻塞,那後續所有的I/O消息都将在隊列中排隊。

(4)由于線程池采用阻塞隊列實作,當隊列積滿之後,後續入隊列的操作将被阻塞。

(5)由于前端隻有一個Accptor線程接收用戶端接入,它被阻塞線上程池的同步阻塞隊列之後,新的用戶端請求消息将被拒絕,用戶端會發生大量的連接配接逾時。

(6)由于幾乎所有的連接配接都逾時,調用者會認為系統已經崩潰,無法接收新的請求消息。 

上一篇: HTML DOM 屬性