Java的BIO,NIO和AIO的區別於演進
作者:公眾號:
前言
Java裡面的IO模型種類較多,主要包括BIO,NIO和AIO,每個IO模型都有不一樣的地方,那麼這些IO模型是如何演變呢,底層的原理又是怎樣的呢? 本文我們就來聊聊。
BIO
BIO全稱是Blocking IO,是JDK1.4之前的傳統IO模型,本身是同步阻塞模式,針對網路通訊都是一請求一應答的方式,雖然簡化了上層的應用開發,但在效能和可靠性方面存在著巨大瓶頸,試想一下如果每個請求都需要新建一個執行緒來專門處理,那麼在高併發的場景下,機器資源很快就會被耗盡,當然,我們可以通過執行緒池來優化這種情況,但即使是這樣,仍然改變不了阻塞IO的根本問題,就是在IO執行的兩個階段都被block了。拿一個read操作來舉例子,在linux中,應用程式向linux發起read操作,會經歷兩個步驟:
第一個階段linux核心首先會把需要讀取的資料載入到作業系統核心的緩衝區中(Linux檔案系統是快取IO,也稱標準IO)
第二個階段應用程式拷貝核心裡面的資料到自己的使用者空間中
如果是socket操作,類似也會經歷兩個步驟:
第一個階段:通常涉及等待網路上的資料分組包到達,然後被複制到核心的緩衝區
第二個階段:把資料從核心緩衝區,從核心緩衝區拷貝到使用者程序的記憶體空間裡面
同步阻塞IO之所以效率低下,就是因為在這兩個階段,使用者的執行緒或者程序都是阻塞的,期間雖然不佔cpu資源,但也意味著該執行緒也不能再幹其他事。有點站著茅坑不拉屎的感覺,自己暫時不用了,也不讓別人用。
圖示如下:
NIO
由於BIO的缺點,導致Java在JDK1.0至JDK3.0中,網路通訊模組的效能一直是短板,所以很多人更傾向於使用C/C++開發高效能服務端。為了強化Java在服務端的市場,終於在JSR-51也就是JDK4.0的時候釋出了Java NIO,可以支援非阻塞IO。並新增了java.nio的包,提供很多非同步開發的API和類庫。
主要的類和介面如下:
(1)進行非同步IO操作的緩衝區ByteBuffer
(2)進行非同步IO操作的管道Pipe
(3)進行各種IO操作的Channel,主要包括ServerSocketChannel和SocketChannel
(4)實現非阻塞IO的多路複用器Selector
NIO主要有buffer、channel、selector三種技術的整合,通過零拷貝的buffer取得資料,每一個客戶端通過channel在selector(多路複用器)上進行註冊。服務端不斷輪詢channel來獲取客戶端的資訊。channel上有connect,accept(阻塞)、read(可讀)、write(可寫)四種狀態標識。根據標識來進行後續操作。所以一個服務端可接收無限多的channel。不需要新開一個執行緒。大大提升了效能。
新的nio類庫,促進了非同步非阻塞程式設計的發展和應用,但仍然有一些不足之處:
(1)沒有統一的檔案屬性,例如讀寫許可權
(2)api能力比較弱,例如目錄的及聯建立和遞迴遍歷,往往需要自己完成。
(3)底層作業系統的一些高階API無法使用
(4)所有的檔案操作都是同步阻塞呼叫,在作業系統層面上並不是非同步檔案讀寫操作。
Java裡面的NIO其實採用了多路複用的IO模式,多路複用的模式在Linux底層其實是採用了select,poll,epoll的機制,這種機制可以用單個執行緒同時監聽多個io埠,當其中任何一個socket的資料準備好了,就能返回通知使用者執行緒進行讀取操作,與阻塞IO阻塞的是每一個使用者的執行緒不一樣的地方是,多路複用只需要阻塞一個使用者執行緒即可,這個使用者執行緒通常我們叫它Selector,其實底層呼叫的是核心的select,這裡面只要任何一個IO操作就緒,就可以喚醒select,然後交由使用者執行緒處理。使用者執行緒讀取資料這個過程仍然是阻塞的,多路複用技術只是在第一個階段可以變為非阻塞呼叫,但在第二個階段拷貝資料到使用者空間,其實還是阻塞的,多路複用技術的最大特點是使用一個執行緒就可以處理很多的socket連線,儘管效能上不一定提升,但支援併發能力卻大大增強了。
圖示如下:
AIO
AIO,其實是NIO的改進優化,也被稱為NIO2.0,在2011年7月,也就是JDK7的版本中釋出,它主要提供了三個方面的改進:
(1)提供了能夠批量獲取檔案屬性的api,通過SPI服務,使得這些API具有平臺無關性。
(2)提供了AIO的功能,支援基於檔案的非同步IO操作和網路套接字的非同步操作
(3)完成了JSR-51定義的通道功能等。
AIO 通過呼叫accept方法,一個會話接入之後再次呼叫(遞迴)accept方法,監聽下一次會話,讀取也不再阻塞,回撥complete方法非同步進行。不再需要selector 使用channel執行緒組來接收。
從NIO上面我們能看到,對於IO的兩個階段的阻塞,只是對於第一個階段有所改善,對於第二個階段在NIO裡面仍然是阻塞的。而真正的理想的非同步非阻塞IO(AAIO)要做的就是,將IO操作的兩個階段都全部交給核心系統完成,使用者執行緒只需要告訴核心,我要讀取一塊資料,請你幫我讀取,讀取完了放在我給你的地址裡面,然後告訴我一聲就可以了。
AIO可以做到真正的非同步的操作,但實現起來比較複雜,支援純非同步IO的作業系統非常少,目前也就windows是IOCP技術實現了,而在Linux上,目前有很多開源的非同步IO庫,例如libevent、libev、libuv,但基本都不是純的非同步IO操作,底層還是是使用的epoll實現的。
圖示如下:
NIO與Netty
既然Java擁有了各種IO體系,那麼為什麼還會出現Netty這種框架呢?
Netty出現的主要原因,如下:
(1)Java NIO類庫和API繁雜眾多,使用麻煩。
(2)Java NIO封裝程度並不高,常常需要配合Java多執行緒程式設計來使用,這是因為NIO程式設計涉及到Reactor模式。
(3)Java NIO異常體系不完善,如客戶端面臨斷連,重連,網路閃斷,半包讀寫,網路阻塞,異常碼流等問題,雖然開發相對容易,但是可靠性和穩定性並不高。
(4)Java NIO本身的bug,修復較慢。
注意,真正的非同步非阻塞io,是需要作業系統層面支援的,在windows上通過IOCP實現了真正的非同步io,所以Java的AIO的非同步在windows平臺才算真正得到了支援,而在Linux系統中,仍然用的是epoll模式,所以在Linux層面上的AIO,並不是真正的或者純的非同步IO,這也是Netty裡面為什麼採用Java的NIO實現的,而並非是AIO,主要原因如下:
(1)AIO在linux上底層實現仍使用EPOLL,與NIO相同,因此在效能上沒有明顯的優勢
(2)Windows的AIO底層實現良好,但Netty的開發者並沒有把Windows作為主要使用平臺,所以優化考慮Linux
總結
本文主要介紹了Java裡面IO模型的演變和發展,這也是Java在服務端領域大放異彩的一個重要原因,瞭解這些知識之後,我們再去學習高效能的Netty框架,將會更加容易。