1. 說一說 I/O

首先來說一下什麼是 I/O?

在計算機系統中 I/O 就是輸入(Input)和輸出(Output)的意思,針對不同的操作對象,可以劃分為磁碟 I/O 模型,網路 I/O 模型,內存映射 I/O,Direct I/O,資料庫 I/O 等,只要具有輸入輸出類型的交互系統都可以認為是 I/O 系統,也可以說 I/O 是整個操作系統數據交換與人機交互的通道,這個概念與選用的開發語言沒有關係,是一個通用的概念。

在如今的系統中 I/O 卻擁有很重要的位置,現在系統都有可能處理大量文件,大量資料庫操作,而這些操作都依賴於系統的 I/O 性能,也就造成了現在系統的瓶頸往往都是由於 I/O 性能造成的。因此,為瞭解決磁碟 I/O 性能慢的問題,系統架構中添加了緩存來提高響應速度;或者有些高端伺服器從硬體級入手,使用了固態硬碟(SSD)來替換傳統機械硬碟;在大數據方面,Spark 越來越多的承擔了實時性計算任務,而傳統的 Hadoop 體系則大多應用在了離線計算與大量數據存儲的場景,這也是由於磁碟 I/O 性能遠不如內存 I/O 性能而造成的格局(Spark 更多的使用了內存,而 MapReduece 更多的使用了磁碟)。因此,一個系統的優化空間,往往都在低效率的 I/O 環節上,很少看到一個系統 CPU、內存的性能是其整個系統的瓶頸。也正因為如此,Java 在 I/O 上也一直在做持續的優化,從 JDK 1.4 開始便引入了 NIO 模型,大大的提高了以往 BIO 模型下的操作效率。

這裡先給出 BIO、NIO、AIO 的基本定義與類比描述:

  • BIO (Blocking I/O):同步阻塞 I/O 模式,數據的讀取寫入必須阻塞在一個線程內等待其完成。這裡使用那個經典的燒開水例子,這裡假設一個燒開水的場景,有一排水壺在燒開水,BIO 的工作模式就是, 叫一個線程停留在一個水壺那,直到這個水壺燒開,纔去處理下一個水壺。但是實際上線程在等待水壺燒開的時間段什麼都沒有做。
  • NIO (New I/O):同時支持阻塞與非阻塞模式,但這裡我們以其同步非阻塞 I/O 模式來說明,那麼什麼叫做同步非阻塞?如果還拿燒開水來說,NIO 的做法是叫一個線程不斷的輪詢每個水壺的狀態,看看是否有水壺的狀態發生了改變,從而進行下一步的操作。
  • AIO ( Asynchronous I/O):非同步非阻塞 I/O 模型。非同步非阻塞與同步非阻塞的區別在哪裡?非同步非阻塞無需一個線程去輪詢所有 IO 操作的狀態改變,在相應的狀態改變後,系統會通知對應的線程來處理。對應到燒開水中就是,為每個水壺上面裝了一個開關,水燒開之後,水壺會自動通知我水燒開了。

進程中的 IO 調用步驟大致可以分為以下四步:

  1. 進程向操作系統請求數據 ;
  2. 操作系統把外部數據載入到內核的緩衝區中;
  3. 操作系統把內核的緩衝區拷貝到進程的緩衝區 ;
  4. 進程獲得數據完成自己的功能 ;

當操作系統在把外部數據放到進程緩衝區的這段時間(即上述的第二,三步),如果應用進程是掛起等待的,那麼就是同步 IO,反之,就是非同步 IO,也就是 AIO 。

補充:Unix 下可用的 5 種 I/O 模型(來自 UNIX 網路編程卷 1,套接字聯網 API)。

  • 阻塞式 I/O
  • 非阻塞式 I/O
  • I/O 復用(select 和 poll)
  • 信號驅動式 I/O(SIGIO)
  • 非同步 I/O(POSIX 的 aio_ 系列函數)

記住如下圖就能非常容易理解 BIO、NIO 和 AIO。

2. BIO(Blocking I/O)同步阻塞 I/O

這是最基本與簡單的 I/O 操作方式,其根本特性是做完一件事再去做另一件事,一件事一定要等前一件事做完,這很符合程序員傳統的順序來開發思想,因此 BIO 模型程序開發起來較為簡單,易於把握。

但是 BIO 如果需要同時做很多事情(例如同時讀很多文件,處理很多 TCP 請求等),就需要系統創建很多線程來完成對應的工作,因為 BIO 模型下一個線程同時只能做一個工作,如果線程在執行過程中依賴於需要等待的資源,那麼該線程會長期處於阻塞狀態,我們知道在整個操作系統中,線程是系統執行的基本單位,在 BIO 模型下的線程阻塞就會導致系統線程的切換,從而對整個系統性能造成一定的影響。當然如果我們只需要創建少量可控的線程,那麼採用 BIO 模型也是很好的選擇,但如果在需要考慮高並發的 web 或者 TCP 伺服器中採用 BIO 模型就無法應對了,如果系統開闢成千上萬的線程,那麼 CPU 的執行時機都會浪費在線程的切換中,使得線程的執行效率大大降低。此外,關於線程這裡說一句題外話,在系統開發中線程的生命週期一定要準確控制,在需要一定規模並發的情形下,盡量使用線程池來確保線程創建數目在一個合理的範圍之內,切莫編寫線程數量創建上限的代碼。

3. NIO (New I/O) 同步非阻塞 I/O

關於 NIO,國內有很多技術博客將英文翻譯成 No-Blocking I/O,非阻塞I/O模型 ,當然這樣就與 BIO 形成了鮮明的特性對比。NIO 本身是基於事件驅動的思想來實現的,其目的就是解決 BIO 的大並發問題,在 BIO 模型中,如果需要並發處理多個 I/O 請求,那就需要多線程來支持,NIO 使用了多路復用器機制,以 socket 使用來說,多路復用器通過不斷輪詢各個連接的狀態,只有在 socket 有流可讀或者可寫時,應用程序才需要去處理它,在線程的使用上,就不需要一個連接就必須使用一個處理線程了,而是隻是有效請求時(確實需要進行 I/O 處理時),才會使用一個線程去處理,這樣就避免了 BIO 模型下大量線程處於阻塞等待狀態的情景。

相對於 BIO 的流,NIO 抽象出了新的通道(Channel)作為輸入輸出的通道,並且提供了緩存(Buffer)的支持,在進行讀操作時,需要使用 Buffer 分配空間,然後將數據從 Channel 中讀入 Buffer 中,對於 Channel 的寫操作,也需要現將數據寫入 Buffer,然後將 Buffer 寫入Channel中。

如下是 NIO 方式進行文件拷貝操作的示例,見下圖:

public static void copyFile(String srcFileName, String dstFileName) throws IOException {
FileInputStream fis = new FileInputStream(srcFileName);
FileOutputStream fos = new FileOutputStream(dstFileName);

FileChannel readChannel = fis.getChannel();
FileChannel writeChannel = fos.getChannel();

ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear(); // 清空緩存區
if (readChannel.read(buffer) == -1) { // 從輸入channel中讀取數據到buffer中
break;
}
buffer.flip(); // 將緩存區遊標置於0
writeChannel.write(buffer); // 將緩存中數據寫入channel中
}

fis.close();
fos.close();
}

通過比較 New IO 的使用方式我們可以發現,新的 IO 操作不再面向 Stream 來進行操作了,改為了通道 Channel,並且使用了更加靈活的緩存區類 Buffer,Buffer 只是緩存區定義介面, 根據需要,我們可以選擇對應類型的緩存區實現類。在 Java NIO 編程中,我們需要理解以下 3 個對象 Channel、Buffer 和 Selector。

Channel

首先說一下 Channel,國內大多翻譯成「通道」。Channel 和 IO 中的 Stream(流)是差不多一個等級的。只不過 Stream 是單向的,譬如:InputStream,OutputStream。而 Channel 是雙向的,既可以用來進行讀操作,又可以用來進行寫操作,NIO 中的 Channel 的主要實現有:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel,通過看名字就可以猜出個所以然來,分別可以對應文件 IO、UDP 和 TCP(Server和Client)。

Buffer

NIO 中的關鍵 Buffer 實現有:ByteBuffer、CharBuffer、DoubleBuffer、 FloatBuffer、IntBuffer、 LongBuffer、ShortBuffer,分別對應基本數據類型:byte、char、double、 float、int、 long、 short。當然 NIO 中還有 MappedByteBuffer, HeapByteBuffer, DirectByteBuffer 等這裡先不具體陳述其用法細節。

說一下 DirectByteBuffer 與 HeapByteBuffer 的區別?

它們是 ByteBuffer 分配內存的兩種方式。HeapByteBuffer 顧名思義其內存空間在 JVM 的 heap(堆)上分配,可以看做是 JDK 對於 byte[] 數組的封裝;而 DirectByteBuffer 則直接利用了系統介面進行內存申請,其內存分配在 c heap 中,這樣就減少了內存之間的拷貝操作,如此一來,在使用 DirectByteBuffer 時,系統就可以直接從內存將數據寫入到 Channel 中,而無需進行 Java 堆的內存申請,複製等操作,提高了性能。既然如此,為什麼不直接使用 DirectByteBuffer,還要來個 HeapByteBuffer?原因在於,DirectByteBuffer 是通過 Full GC 來回收內存的,DirectByteBuffer 會自己檢測情況而調用 System.gc(),但是如果參數中使用了 DisableExplicitGC 那麼就無法回收該快內存了,-XX:+DisableExplicitGC 標誌自動將 System.gc() 調用轉換成一個空操作,就是應用中調用 System.gc() 會變成一個空操作,那麼如果設置了就需要我們手動來回收內存了,所以 DirectByteBuffer 使用起來相對於完全託管於 Java 內存管理的 Heap ByteBuffer 來說更複雜一些,如果用不好可能會引起 OOM。Direct ByteBuffer 的內存大小受 -XX:MaxDirectMemorySize JVM 參數控制(默認大小 64M),在 DirectByteBuffer 申請內存空間達到該設置大小後,會觸發 Full GC。

Selector

Selector 是 NIO 相對於 BIO 實現多路復用的基礎,Selector 運行單線程處理多個 Channel,如果你的應用打開了多個通道,但每個連接的流量都很低,使用 Selector 就會很方便。例如在一個聊天伺服器中,要使用 Selector , 得向 Selector 註冊 Channel,然後調用它的 select() 方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新的連接進來、數據接收等。

這裡我們再來看一個 NIO 模型下的 TCP 伺服器的實現,我們可以看到 Selector 正是NIO模型下 TCP Server 實現 IO 復用的關鍵,請仔細理解下段代碼 while 循環中的邏輯,見下圖:

4. AIO (Asynchronous I/O) 非同步非阻塞 I/O

Java AIO 就是 Java 作為對非同步 IO 提供支持的 NIO.2,Java NIO2 (JSR 203) 定義了更多的 New I/O APIs, 提案2003提出,直到2011年才發布, 最終在 JDK 7 中才實現。JSR 203 除了提供更多的文件系統操作 API(包括可插拔的自定義的文件系統), 還提供了對 socket 和文件的非同步 I/O 操作。 同時實現了JSR-51 提案中的 socket channel 全部功能,包括對綁定,option 配置的支持以及多播 multicast 的實現。

從編程模式上來看 AIO 相對於 NIO 的區別在於,NIO 需要使用者線程不停的輪詢 IO 對象,來確定是否有數據準備好可以讀了,而 AIO 則是在數據準備好之後,才會通知數據使用者,這樣使用者就不需要不停地輪詢了。當然 AIO 的非同步特性並不是 Java 實現的偽非同步,而是使用了系統底層 API 的支持,在 Unix 系統下,採用了 epoll IO 模型,而 windows 便是使用了 IOCP 模型。關於 Java AIO,本篇只做一個拋磚引玉的介紹,如果你在實際工作中用到了,那麼可以參考 Netty 在高並發下使用 AIO 的相關技術。

總 結

IO 實質上與線程沒有太多的關係,但是不同的 IO 模型改變了應用程序使用線程的方式,NIO 與 AIO 的出現解決了很多 BIO 無法解決的並發問題,當然任何技術拋開適用場景都是耍流氓,複雜的技術往往是為瞭解決簡單技術無法解決的問題而設計的,在系統開發中能用常規技術解決的問題,絕不用複雜技術,否則大大增加系統代碼的維護難度,學習 IT 技術不是為了炫技,而是要實實在在解決問題。

以Java的視角來聊聊BIO、NIO與AIO的區別??

mp.weixin.qq.com
圖標
http://www.importnew.com/21383.html?

www.importnew.com


推薦閱讀:
相關文章