Java I/O模型及其底層原理

果凍迪迪發表於2020-06-10

  Java I/O是Java基礎之一,在面試中也比較常見,在這裡我們嘗試通過這篇文章闡述Java I/O的基礎概念,幫助大家更好的理解Java I/O。
  在剛開始學習Java I/O時,我很迷惑,因為網上絕大多數的文章都是講解Linux網路I/O模型的,那時我總是搞不明白和Java I/O的關係。後來查了看了好多,才明白Java I/O的原理是以Linux網路I/O模型為基礎的,理解了Linux網路I/O模型再學習Java I/O就很方便了,所以這篇文章,我們先來了解I/O的基本概念,再學習Linux網路I/O模型,最後再看Java中的幾種I/O。

什麼是I/O?

  I/O是Input、Output的縮寫,即對應計算機中的輸入輸出,以一次檔案讀取為例,我們需要將磁碟上的資料讀取到使用者空間,那麼這次資料轉移操作其實就是一次I/O操作,更具體的說是一次檔案I/O。我們瀏覽網頁,其中在請求一個網頁時,伺服器通過網路把資料傳送給我們,此時程式將資料從TCP緩衝區複製到使用者空間,那麼這次資料轉移操作其實也是一次I/O操作,更具體的說是一次網路I/O。I/O到處都在,十分重要,Java對I/O對底層作業系統的各種I/O模型進行了封裝,使我們可以輕鬆開發。

Linux網路I/O模型

  根據UNIX網路程式設計對I/O模型的分類,UNIX提供了5種I/O模型,分別是:阻塞I/O(Blocking I/O)、非阻塞I/O(Non-Blacking I/O)、I/O多路複用模型(I/O Multiplexing)、訊號驅動式I/O(Signal Driven I/O)、非同步I/O(Asynchronous I/O)。我們逐步瞭解一下其基本原理。

阻塞I/O(Blocking I/O)

  阻塞I/O是最早最基礎的I/O模型,其在讀寫資料過程中會阻塞。通過下圖我們可以看到,當使用者程式呼叫了recvfrom這個系統呼叫後,核心開始第一階段的資料準備工作,直到核心等待資料準備完成,然後開始第二階段的將資料從核心複製到使用者空間的工作,最後核心返回結果。整個過程中使用者程式都是阻塞的,直到最後返回結果後才接觸阻塞block狀態。阻塞I/O模型適用於併發量小且對時延不敏感的系統。

非阻塞I/O(Non-Blacking I/O)

  當使用者程式呼叫recvfrom這個系統呼叫後,如果核心尚未準備好資料,此時不再阻塞使用者程式,而是立即返回一個EWOULDBLOCK錯誤。使用者程式會不斷髮起系統呼叫直到核心中資料被準備好(輪詢),此時將執行第二階段的將資料從核心複製到使用者空間的工作,然後核心返回結果。非阻塞I/O模型不斷地輪詢往往需要耗費大量cpu時間。

I/O多路複用模型(I/O Multiplexing)

  I/O多路複用的優點在於單個程式可以同時處理多個網路連線的I/O,其基本原理就是select/epoll函式可以不斷的輪詢其負責的所有socket,當某個socket有資料到達時,就通知使用者程式。
  如下圖所示,當使用者程式呼叫select函式時,整個程式會被阻塞block住,但是這裡的阻塞不是被socket I/O阻塞,而是被select這個函式阻塞。同時核心會監聽改select負責的所有socket(這裡的socket一般設定為non-blocking),當任何一個socket中的資料準備好時,select就會返回給使用者程式,這時候使用者程式再此發起一個系統呼叫,將資料從核心複製到使用者空間,並返回結果。
  對比I/O多路複用模型和阻塞I/O模型的流程,多路複用多了一個系統呼叫來完成select環節,除此之外沒有太大的不同。Select的優勢在於它可以同時處理多個connection,但是會多一個系統呼叫。多路複用本質上也不是非阻塞的。

訊號驅動式I/O(Signal Driven I/O)

  首先我們開啟socket的訊號驅動I/O功能,然後使用者程式發起sigaction系統呼叫給核心後立即返回並可繼續處理其他工作。收到sigaction系統呼叫的核心在將資料準備好後會按照要求產生一個signo訊號通知給使用者程式。然後使用者程式再發起recvfrom系統呼叫,完成資料從核心到使用者空間的複製,並返回最終結果。其基礎原理圖示如下:

非同步I/O(Asynchronous I/O)

  使用者程式向核心發起系統呼叫後,就可以開始去做其他事情了。核心收到非同步I/O的系統呼叫後,會直接retrun,所以這裡不會對使用者程式有阻塞。之後核心等待資料準備完成後會繼續將資料從核心拷貝到使用者空間(具體動作可以由非同步I/O呼叫定義),然後核心回給使用者程式傳送一個signal,告訴使用者程式I/O操作完成了,整個過程不會導致使用者請求程式阻塞。
  訊號驅動I/O模型是核心通知我們可以發起I/O操作了,而非同步I/O模式是核心告訴我們I/O操作已經完成了。

  以上就是Linux的5種網路I/O模型,其中前4中都是同步I/O模型,他們真正的I/O操作環節都會將程式阻塞,只有最後一種非同步I/O模型是非同步I/O操作。

Java中的I/O模型

  在JDK1.4之前,基於Java的所有socket通訊都是使用阻塞I/O(BIO),JDK1.4提供了了非阻塞I/O(NIO)功能,不過雖然名字叫做NIO,實際底層模型是I/O多路複用,JDK1.7提供了針對非同步I/O(AIO)功能。

BIO

  BIO簡化了上層開發,但是效能瓶頸問題嚴重,對高併發第時延支援差。
基於訊息佇列和執行緒池技術優化的BIO模式雖然可以對高併發支援有一定幫助,但是還是受限於執行緒池大小和執行緒池阻塞佇列大小的制約,當併發數超過執行緒池的處理能力時,部分請求法務繼續處理,會導致客戶端連線超時,影響使用者體驗。

NIO

  NIO彌補了BIO的不足,簡單說就是通過selector不斷輪詢註冊在自己上面的channel,如果channel上面有新的連線讀寫時間時就會被輪詢出來,一個selector上面可以註冊多個channel,一個執行緒就可以負責selector的輪詢,這樣就可以支援成千上萬的連線。Selector就是一個輪詢器,channel是一個通道,通過它來讀取或者寫入資料,通道是雙向的,可以用於讀、寫、讀和寫。Buffer用來和channel互動,資料通過channel進出buffer。
NIO的優點是可以可靠性好以及高併發低時延,但是使用NIO的程式碼開發較為複雜。

AIO

  AIO,或者說叫做NIO2.0,引入了非同步channel的概念,提供了非同步檔案channel和非同步socket channel的實現,開發者可以通過Future類來表示非同步操作的結果,也可以在執行非同步操作時傳入一個channels,實現CompletionHandler介面作為回撥。AIO不用開發者單獨開發獨立執行緒的selector,非同步回撥操作有JDK地城思安城池負責驅動,開發起來比NIO簡單一些,同時保持了高可靠高併發低時延的優點。

參考:
https://blog.csdn.net/historyasamirror/article/details/5778378
https://juejin.im/post/5cce5019e51d453a506b0ebf

相關文章