BIO,NIO,AIO 總結
Java 中的 BIO、NIO和 AIO 理解為是 Java 語言對操作系統的各種 IO 模型的封裝。程序員在使用這些 API 的時候,不需要關心操作系統層面的知識,也不需要根據不同操作系統編寫不同的代碼。只需要使用Java的API就可以了。
在講 BIO,NIO,AIO 之前先來回顧一下這樣幾個概念:同步與異步,阻塞與非阻塞。
同步與異步
-
同步: 同步就是發起一個調用后,被調用者未處理完請求之前,調用不返回。
-
異步: 異步就是發起一個調用后,立刻得到被調用者的回應表示已接收到請求,但是被調用者並沒有返回結果,此時我們可以處理其他的請求,被調用者通常依靠事件,回調等機制來通知調用者其返回結果。
同步和異步的區別最大在於異步的話調用者不需要等待處理結果,被調用者會通過回調等機制來通知調用者其返回結果。
阻塞和非阻塞
-
阻塞: 阻塞就是發起一個請求,調用者一直等待請求結果返回,也就是當前線程會被掛起,無法從事其他任務,只有當條件就緒才能繼續。
-
非阻塞: 非阻塞就是發起一個請求,調用者不用一直等着結果返回,可以先去干其他事情。
那么同步阻塞、同步非阻塞和異步非阻塞又代表什么意思呢?
舉個生活中簡單的例子,你媽媽讓你燒水,小時候你比較笨啊,在哪里傻等着水開(同步阻塞)。等你稍微再長大一點,你知道每次燒水的空隙可以去干點其他事,然后只需要時不時來看看水開了沒有(同步非阻塞)。后來,你們家用上了水開了會發出聲音的壺,這樣你就只需要聽到響聲后就知道水開了,在這期間你可以隨便干自己的事情,你需要去倒水了(異步非阻塞)。
一、BIO的理解
首先我們通過通信模型圖來熟悉下BIO的服務端通信模型:采用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端的連接請求之后為每個客戶端創建一個新的線程進行鏈路處理,處理完成之后,通過輸出流返回應答給客戶端,線程銷毀。這就是典型的一請求一應答通信模型。這個是在多線程情況下執行的。當在單線程環境下時,在while循環中服務端會調用accept方法等待接收客戶端的連接請求,一旦接收到一個連接請求,就可以建立socket,並在該socket上進行讀寫操作,此時不能再接收其它客戶端的連接請求,只能等待同當前連接的客戶端的操作執行完成。

二、偽異步I/O編程
為了解決同步阻塞I/O面臨的一個鏈路需要一個線程處理的問題,后來有人對它的線程模型進行了優化,后端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數M:線程池最大線程數N的比例關系,其中M可以遠遠大於N,通過線程池可以靈活的調配線程資源。設置線程的最大值,防止由於海量並發接入導致線程耗盡。
采用線程池和任務隊列可以實現一種叫做偽異步的I/O通信框架。模型圖如下。
當有新的客戶端接入時,將客戶端的Socket封裝成一個Task(該任務實現Java.lang.Runnablle接口)投遞到后端的線程池中進行處理,JDK的線程池維護一個消息隊列和N個活躍線程對消息隊列中的任務進行處理。由於線程池可以設置消息隊列的大小和最大線程數,因此,它的資源占用是可控的,無論多少個客戶端並發訪問,都不會導致資源的耗盡和宕機。
由於線程池和消息隊列都是有界的,因此,無論客戶端並發連接數多大,它都不會導致線程個數過於膨脹或者內存溢出,相對於傳統的一連接一線程模型,是一種改良。
偽異步I/O通信框架采用了線程池實現,因此避免了為每個請求都創建一個獨立線程造成的線程資源耗盡問題。但是由於它底層的通信依然采用同步阻塞模型,因此無法從根本上解決問題。
通過對輸入和輸出流的API文檔進行分析,我們了解到讀和寫操作都是同步阻塞的,阻塞的時間取決於對方IO線程的處理速度和網絡IO的傳輸速度,本質上講,我們無法保證生產環境的網絡狀況和對端的應用程序能足夠快,如果我們的應用程序依賴對方的處理速度,它的可靠性就會非常差。
三、NIO編程(非阻塞IO)
與Socket類和ServerSocket類相對應,NIO也提供了SocketChannel和ServerSocketChannel兩種不同的套接字通道實現,在JDK1.4中引入。這兩種新增的通道都支持阻塞和非阻塞兩種模式。阻塞模式使用非常簡單,但是性能和可靠性都不好,非阻塞模式則正好相反。我們可以根據自己的需求來選擇合適的模式,一般來說,低負載、低並發的應用程序可以選擇同步阻塞IO以降低編程復雜度,但是對於高負載、高並發的網絡應用,需要使用NIO的非阻塞模式進行開發。
首先來了解一些概念
(1)緩沖區Buffer
Buffer是一個對象,它包含一些要寫入或者要讀出的數據,在NIO庫中,所有數據都是用緩沖區處理的。在讀取數據時,它是直接讀到緩沖區中的;在寫入數據時,寫入到緩沖區中,任何時候訪問NIO中的數據,都是通過緩沖區進行操作。
緩沖區實質上是一個數組。通常它是一個字節數組(ByteBuffer),也可以使用其他種類的數組,但是一個緩沖區不僅僅是一個數組,緩沖區提供了對數據的結構化訪問以及維護讀寫位置(limit)等信息。常用的有ByteBuffer,其它還有CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer
(2)通道Channel
Channel是一個通道,可以通過它讀取和寫入數據,它就像自來水管一樣,網絡數據通過Channel讀取和寫入。通道與流的不同之處在於通道是雙向的,流只是一個方向上移動(一個流必須是InputStream或者OutputStream的子類),而且通道可以用於讀、寫或者用於讀寫。同時Channel是全雙工的,因此它可以比流更好的映射底層操作系統的API。特別是在Unix網絡編程中,底層操作系統的通道都是全雙工的,同時支持讀寫操作。我們常用到的ServerSocketChannnel和SocketChannel都是SelectableChannel的子類。
(3)多路復用器Selector
多路復用器Selector是Java NIO編程的基礎,多路復用器提供選擇已經就緒的任務的能力,簡單的說,Selector會不斷的輪詢注冊在其上的Channel,如果某個Channel上面有新的TCP連接接入、讀和寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,然后通過SelectionKey可以獲取就緒Channel的集合,進行后續的I/O操作。
一個多用復用器Selector可以同時輪詢多個Channel,由於JDK使用了epoll()代替傳統的select實現,所以它並沒有最大連接句柄1024/2048的限制,這也意味着只需要一個線程負責Selector的輪詢,就可以接入成千上萬的客戶端。


盡管NIO編程難度確實比同步阻塞BIO大很多,但是我們要考慮到它的優點:
(1)客戶端發起的連接操作是異步的,可以通過在多路復用器注冊OP_CONNECT等后續結果,不需要像之前的客戶端那樣被同步阻塞。
(2)SocketChannel的讀寫操作都是異步的,如果沒有可讀寫的數據它不會同步等待,直接返回,這樣IO通信線程就可以處理其它的鏈路,不需要同步等待這個鏈路可用。
(3)線程模型的優化:由於JDK的Selector在Linux等主流操作系統上通過epoll實現,它沒有連接句柄數的限制(只受限於操作系統的最大句柄數或者對單個進程的句柄限制),這意味着一個Selector線程可以同時處理成千上萬個客戶端連接,而且性能不會隨着客戶端的增加而線性下降,因此,它非常適合做高性能、高負載的網絡服務器。
四、AIO(異步非阻塞IO)
JDK1.7升級了NIO類庫,升級后的NIO類庫被稱為NIO2.0。也就是我們要介紹的AIO。NIO2.0引入了新的異步通道的概念,並提供了異步文件通道和異步套接字通道的實現。異步通道提供兩種方式獲取操作結果。
(1)通過Java.util.concurrent.Future類來表示異步操作的結果;
(2)在執行異步操作的時候傳入一個Java.nio.channels.
CompletionHandler接口的實現類作為操作完成的回調。
NIO2.0的異步套接字通道是真正的異步非阻塞IO,它對應UNIX網絡編程中的事件驅動IO(AIO),它不需要通過多路復用器(Selector)對注冊的通道進行輪詢操作即可實現異步讀寫,從而簡化了NIO的編程模型。
我們可以得出結論:異步Socket Channel是被動執行對象,我們不需要想NIO編程那樣創建一個獨立的IO線程來處理讀寫操作。對於AsynchronousServerSocketChannel和AsynchronousSocketChannel,它們都由JDK底層的線程池負責回調並驅動讀寫操作。正因為如此,基於NIO2.0新的異步非阻塞Channel進行編程比NIO編程更為簡單。

NIO適合處理連接數目特別多,但是連接比較短(輕操作)的場景,Jetty,Mina,ZooKeeper等都是基於java nio實現。
BIO方式適用於連接數目比較小且固定的場景,這種方式對服務器資源要求比較高,並發局限於應用中。
