Java BIO NIO AIO 詳解

一、瞭解Unix網絡編程5種I/O模型

1.1、阻塞式I/O模型

阻塞IO

阻塞I/O(blocking I/O)模型,進程調用recvfrom,其系統調用直到數據報到達且被拷貝到應用進程的緩衝區中或者發生錯誤才返回。進程從調用recvfrom開始到它返回的整段時間內是被阻塞的。

1.2、非阻塞式I/O模型

非阻塞IO

當一個應用進程像這樣對一個非阻塞描述字循環調用recvfrom時,我們稱之爲輪詢(polling)。應用進程持續輪詢內核,以查看某個操作是否就緒。

1.3、I/O多路複用(事件驅動)模型

IO複用模型

1.4、信號驅動式I/O(SIGIO)

信號驅動IO

1.5、異步I/O模型

異步IO

1.6、I/O模型的比較:

模型的比較

根據上述5種IO模型,前4種模型-阻塞IO、非阻塞IO、IO複用、信號驅動IO都是同步I/O模型,因爲其中真正的I/O操作(recvfrom)將阻塞進程,在內核數據copy到用戶空間時都是阻塞的。

1.7、同步IO、異步IO、阻塞IO、非阻塞IO

一個IO操作可以分爲兩個步驟:發起IO請求和實際的IO操作
例如:
1、操作系統的一次寫操作分爲兩步:將數據從用戶空間拷貝到系統空間;從系統空間往網卡寫。
2、一次讀操作分爲兩步:將數據從網卡拷貝到系統空間;將數據從系統空間拷貝到用戶空間。

阻塞IO和非阻塞IO的區別在於第一步,發起IO請求是否會被阻塞,如果阻塞直到完成那麼就是傳統的阻塞IO,如果不阻塞,那麼就是非阻塞IO。

同步IO和異步IO的區別就在於第二個步驟是否阻塞,如果實際的IO讀寫阻塞請求進程,那麼就是同步IO,因此阻塞IO、非阻塞IO、IO複用、信號驅動IO都是同步IO,如果不阻塞,而是操作系統做完IO兩個階段的操作再將結果返回,那麼就是異步IO。

1.8、IO多路複用

IO多路複用,就是通過一種機制,一個進程可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。

從流程上來看,使用select函數進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操作,效率更差。但是,使用select以後最大的優勢是用戶可以在一個線程內同時處理多個socket的IO請求。用戶可以註冊多個socket,然後不斷地調用select讀取被激活的socket,即可達到在同一個線程內同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。

IO多路複用方式允許單線程內處理多個IO請求,但是每個IO請求的過程還是阻塞的(在select函數上阻塞),平均時間甚至比同步阻塞IO模型還要長。如果用戶線程只註冊自己感興趣的socket或者IO請求,然後去做自己的事情,等到數據到來時再進行處理,則可以提高CPU的利用率。
由於select函數是阻塞的,因此多路IO複用模型也被稱爲異步阻塞IO模型。注意,這裏的所說的阻塞是指select函數執行時線程被阻塞,而不是指socket。一般在使用IO多路複用模型時,socket都是設置爲NONBLOCK的,不過這並不會產生影響,因爲用戶發起IO請求時,數據已經到達了,用戶線程一定不會被阻塞。
IO多路複用是最常使用的IO模型,但是其異步程度還不夠“徹底”,因爲它使用了會阻塞線程的select系統調用。因此IO多路複用只能稱爲異步阻塞IO,而非真正的異步IO。

非阻塞IO
展示了非阻塞IO如何讓你使用一個selector區處理多個連接.

1.9、select、poll、epoll

Linux支持IO多路複用的系統調用有select、poll、epoll,這些調用都是內核級別的。但select、poll、epoll本質上都是同步I/O,先是block住等待就緒的socket,再是block住將數據從內核拷貝到用戶內存。

select、poll、epoll之間的區別,如下表:
screenshot.png

1.10、兩種I/O多路複用模式:Reactor和Proactor

在這兩種模式下的事件多路分離器反饋給程序的信息是不一樣的:
1.Reactor模式下說明你可以進行讀寫(收發)操作了。
2.Proactor模式下說明已經完成讀寫(收發)操作了,具體內容在給定緩衝區中,可以對這些內容進行其他操作了。
Reactor關注的是I/O操作的就緒事件,而Proactor關注的是I/O操作的完成事件

一般地,I/O多路複用機制都依賴於一個事件多路分離器(Event Demultiplexer)。分離器對象可將來自事件源的I/O事件分離出來,並分發到對應的read/write事件處理器(Event Handler)。

Reactor模式採用同步IO,而Proactor採用異步IO。

在Reactor中,事件分離器負責等待文件描述符或socket爲讀寫操作準備就緒,然後將就緒事件傳遞給對應的處理器,最後由處理器負責完成實際的讀寫工作。

而在Proactor模式中,處理器或者兼任處理器的事件分離器,只負責發起異步讀寫操作。IO操作本身由操作系統來完成。傳遞給操作系統的參數需要包括用戶定義的數據緩衝區地址和數據大小,操作系統才能從中得到寫出操作所需數據,或寫入從socket讀到的數據。事件分離器捕獲IO操作完成事件,然後將事件傳遞給對應處理器。比如,在windows上,處理器發起一個異步IO操作,再由事件分離器等待IOCompletion事件。典型的異步模式實現,都建立在操作系統支持異步API的基礎之上,我們將這種實現稱爲“系統級”異步或“真”異步,因爲應用程序完全依賴操作系統執行真正的IO工作。

Reactor和Proactor模式的主要區別就是真正的讀取和寫入操作是有誰來完成的,Reactor中需要應用程序自己讀取或者寫入數據,而Proactor模式中,應用程序不需要進行實際的讀寫過程,它只需要從緩存區讀取或者寫入即可,操作系統會讀取緩存區或者寫入緩存區到真正的IO設備.

二、Java NIO

NIO,有人稱之爲New I/O,因爲它相對於之前的I/O類庫是新增的,所以被稱爲New I/O。但是,由於之前老的 I/O 類庫是阻塞 I/O,New I/O類庫的目標就是要讓Java支持非阻塞 I/O,所以,更多的人喜歡稱之爲非阻塞 I/ O(Non-block I/O)。

2.1、對NIO的非阻塞的理解

注意,select是阻塞的,無論是通過操作系統的通知(epoll)還是不停的輪詢(select,poll),這個函數是阻塞的。所以你可以放心大膽地在一個while(true)裏面調用這個函數而不用擔心CPU空轉。

NIO採用Reactor模式,一個Reactor線程聚合一個多路複用器Selector,它可以同時註冊、監聽和輪詢成百上千個Channel,一個IO線程可以同時併發處理N個客戶端連接,線程模型優化爲1:N(N < 進程可用的最大句柄數)或者M : N (M通常爲CPU核數 + 1, N < 進程可用的最大句柄數)。

JAVA NIO 不是同步非阻塞I/O嗎,爲什麼說JAVA NIO提供了基於Selector的異步網絡I/O?
java nio的io模型是同步非阻塞,這裏的同步異步指的是真正io操作(數據內核態用戶態的拷貝)是否需要進程參與。
而說java nio提供了異步處理,這個異步應該是指編程模型上的異步。基於reactor模式的事件驅動,事件處理器的註冊和處理器的執行是異步的。

AIO(Async I/O)裏面會更進一步:不但等待就緒是非阻塞的,就連數據從網卡到內存的過程也是異步的。
換句話說,BIO裏用戶最關心“我要讀”,NIO裏用戶最關心"我可以讀了",在AIO模型裏用戶更需要關注的是“讀完了”。
NIO一個重要的特點是:socket主要的讀、寫、註冊和接收函數,在等待就緒階段都是非阻塞的,真正的I/O操作是同步的(消耗CPU但性能非常高)。

2.2、如何結合事件模型使用NIO非阻塞特性

BIO模型,之所以需要多線程,是因爲在進行I/O操作的時候,一是沒有辦法知道到底能不能寫、能不能讀,只能"傻等",即使通過各種估算,算出來操作系統沒有能力進行讀寫,也沒法在socket.read()和socket.write()函數中返回,這兩個函數無法進行有效的中斷。所以除了多開線程另起爐竈,沒有好的辦法利用CPU。

NIO的讀寫函數可以立刻返回,這就給了我們不開線程利用CPU的最好機會:如果一個連接不能讀寫(socket.read()返回0或者socket.write()返回0),我們可以把這件事記下來,記錄的方式通常是在Selector上註冊標記位,然後切換到其它就緒的連接(channel)繼續進行讀寫。

我們大概可以總結出NIO是怎麼解決掉線程的瓶頸並處理海量連接的:

NIO由原來的阻塞讀寫(佔用線程)變成了單線程輪詢事件,找到可以進行讀寫的網絡描述符進行讀寫。除了事件的輪詢是阻塞的(沒有可乾的事情必須要阻塞),剩餘的I/O操作都是純CPU操作,沒有必要開啓多線程。
並且由於線程的節約,連接數大的時候因爲線程切換帶來的問題也隨之解決,進而爲處理海量連接提供了可能。

2.3、理解異步非阻塞I/O

很多人喜歡將JDK1.4提供的NIO框架稱爲異步非阻塞I/O,但是,如果嚴格按照UNIX網絡編程模型和JDK的實現進行區分,實際上它只能被稱爲非阻塞I/O,不能叫異步非阻塞I/O。在早期的JDK1.4和1.5 update10版本之前,JDK的Selector基於select/poll模型實現,它是基於I/O複用技術的非阻塞I/O,不是異步I/O。在JDK1.5 update10和Linux core2.6以上版本,Sun優化了Selctor的實現,它在底層使用epoll替換了select/poll,上層的API並沒有變化,可以認爲是JDK NIO的一次性能優化,但是它仍舊沒有改變I/O的模型。
由JDK1.7提供的NIO2.0,新增了異步的套接字通道,它是真正的異步I/O,在異步I/O操作的時候可以傳遞信號變量,當操作完成之後會回調相關的方法,異步I/O也被稱爲AIO。
NIO類庫支持非阻塞讀和寫操作,相比於之前的同步阻塞讀和寫,它是異步的,因此很多人習慣於稱NIO爲異步非阻塞I/O,包括很多介紹NIO編程的書籍也沿用了這個說法。爲了符合大家的習慣,我們也將NIO稱爲異步非阻塞I/O或者非阻塞I/O。

screenshot.png

三、Java NIO的核心組成

3.1、通道(Channel) 和 緩衝區(Buffer)

基本上,所有的 IO 在NIO 中都從一個Channel 開始。Channel 有點象流。 數據可以從Channel讀到Buffer中,也可以從Buffer 寫到Channel中。這裏有個圖示:
screenshot.png

3.2、多路複用器(Selector)

Selector允許單線程處理多個Channel。如果你的應用打開了多個連接(通道),但每個連接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中。

這是在一個單線程中使用一個Selector處理3個Channel的圖示:
selector

要使用Selector,得向Selector註冊Channel,然後調用它的select()方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新連接進來,數據接收等。

四、總結

最後總結一下到底NIO給我們帶來了些什麼:
事件驅動模型
避免多線程
單線程處理多任務
非阻塞I/O,I/O讀寫不再阻塞,而是返回0
基於block的傳輸,通常比基於流的傳輸更高效
更高級的IO函數,zero-copy
IO多路複用大大提高了Java網絡應用的可伸縮性和實用性

 

 

 


從編程語言層面

BIO | NIO | AIO 以Java的角度,理解如下:

  • BIO,同步阻塞式IO,簡單理解:一個線程處理一個連接,發起和處理IO請求都是同步的
  • NIO,同步非阻塞IO,簡單理解:一個線程處理多個連接,發起IO請求是非阻塞的但處理IO請求是同步的
  • AIO,異步非阻塞IO,簡單理解:一個有效請求一個線程,發起和處理IO請求都是異步的

 

BIO

在JDK1.4之前,用Java編寫網絡請求,都是建立一個ServerSocket,然後,客戶端建立Socket時就會詢問是否有線程可以處理,如果沒有,要麼等待,要麼被拒絕。即:一個連接,要求Server對應一個處理線程。

NIO

在Java裏的由來,在JDK1.4及以後版本中提供了一套API來專門操作非阻塞I/O,我們可以在java.nio包及其子包中找到相關的類和接口。由於這套API是JDK新提供的I/O API,因此,也叫New I/O,這就是包名nio的由來。這套API由三個主要的部分組成:緩衝區(Buffers)、通道(Channels)和非阻塞I/O的核心類組成。在理解NIO的時候,需要區分,說的是New I/O還是非阻塞IO,New I/O是Java的包,NIO是非阻塞IO概念。這裏講的是後面一種。

NIO本身是基於事件驅動思想來完成的,其主要想解決的是BIO的大併發問題: 在使用同步I/O的網絡應用中,如果要同時處理多個客戶端請求,或是在客戶端要同時和多個服務器進行通訊,就必須使用多線程來處理。也就是說,將每一個客戶端請求分配給一個線程來單獨處理。這樣做雖然可以達到我們的要求,但同時又會帶來另外一個問題。由於每創建一個線程,就要爲這個線程分配一定的內存空間(也叫工作存儲器),而且操作系統本身也對線程的總數有一定的限制。如果客戶端的請求過多,服務端程序可能會因爲不堪重負而拒絕客戶端的請求,甚至服務器可能會因此而癱瘓。

NIO基於Reactor,當socket有流可讀或可寫入socket時,操作系統會相應的通知引用程序進行處理,應用再將流讀取到緩衝區或寫入操作系統。 
也就是說,這個時候,已經不是一個連接就要對應一個處理線程了,而是有效的請求,對應一個線程,當連接沒有數據時,是沒有工作線程來處理的。

AIO

與NIO不同,操作系統負責處理內核區/用戶區的內存數據遷移和真正的IO操作,應用程序只須直接調用API的read或write方法即可。這兩種方法均爲異步的,對於讀操作而言,當有流可讀取時,操作系統會將可讀的流傳入read方法的緩衝區,並通知應用程序;對於寫操作而言,當操作系統將write方法傳遞的流寫入完畢時,操作系統主動通知應用程序。 
即可以理解爲,read/write方法都是異步的,完成後會主動調用回調函數。 
在JDK1.7中,這部分內容被稱作NIO.2,主要在java.nio.channels包下增加了下面四個異步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

其中的read/write方法,會返回一個帶回調函數的對象,當執行完讀取/寫入操作後,直接調用回調函數。


實現原理

說道實現原理,還要從操作系統的IO模型上了解

按照《Unix網絡編程》的劃分,IO模型可以分爲:阻塞IO、非阻塞IO、IO複用、信號驅動IO和異步IO,按照POSIX標準來劃分只分爲兩類:同步IO和異步IO。如何區分呢?首先一個IO操作其實分成了兩個步驟:發起IO請求和實際的IO操作,同步IO和異步IO的區別就在於第二個步驟是否阻塞,如果實際的IO讀寫阻塞請求進程,那麼就是同步IO,因此阻塞IO、非阻塞IO、IO複用、信號驅動IO都是同步IO,如果不阻塞,而是操作系統幫你做完IO操作再將結果返回給你,那麼就是異步IO。阻塞IO和非阻塞IO的區別在於第一步,發起IO請求是否會被阻塞,如果阻塞直到完成那麼就是傳統的阻塞IO,如果不阻塞,那麼就是非阻塞IO。

可以理解的說明是:在Linux 2.6以後,java NIO的實現,是通過epoll來實現的,這點可以通過jdk的源代碼發現。而AIO,在windows上是通過IOCP實現的,在linux上通過新的API來實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章