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