1. 程式人生 > >Java IO & NIO & NIO2

Java IO & NIO & NIO2

概覽

IO是Java中的最重要的一個部分. 其中, java.io是所有程式設計者都應該掌握的IO方式. 在Java 1.4中, NIO被引入, 它引進了一種新的相對於流模型的新的IO模型, 以為非阻塞IO提供支援. 在Java 7中, NIO2又在NIO的基礎上, 引入了對非同步IO的支援. 在這篇文章我, 我將對這幾種IO方式進行一個比較系統的說明及總結, 同時, 分析每一種IO模型的適用範圍.

流IO

流IO是一種最為簡潔的IO方式, 幾乎所有的程式語言在其標準庫中都提供了對流IO的支援, 比如C的FILE, C++的iostream. 同樣, 在Java中, 流IO是最為基礎也是最為廣泛使用的IO方式, 一般來說, 大家對這種方式都比較熟悉了, 總的來說, Java提供Byte輸入流/Byte輸出流/Char輸入流/Char輸出流, 同時, 又提供了一系列的Decorator類來在基礎流的功能之上, 新增新的功能, 如Buffering.

用下面的四張圖可以很好地概括整個流IO的相關類.

輸入流
這裡寫圖片描述
輸出流
這裡寫圖片描述
字元輸入流
這裡寫圖片描述
字元輸出流
這裡寫圖片描述

NIO

一般來說, 流IO表現得很好, 對於大部分的IO場景, 它都能適應. 但是, 由於它的阻塞性, 每一個流的讀寫都需要佔用一個執行緒. 這意味著, 流IO的可伸縮性很差. 因此, 引入非阻塞IO就再正常不過了. 實際上, NIO就是IO Multiplexing在Java中的實現. IO Multiplexing在系統級語言如C/C++中應用了很長時間. 使用IO Multiplexing, IO的伸縮性大大提高, 使用單個執行緒, 就可以處理大量的IO物件.

在介紹NIO的非阻塞IO之前, 先大致瞭解一下NIO提供的IO模型. NIO的概念概念有三個, Buffers/Channels/Selectors. 其中, Channels是輸入/輸出的管道, 所有的讀寫操作都需要通過它來完成. Channel讀寫的粒度是Block, 而不是像流IO一樣, 提供一個位元組流或者字元流的抽象. 這個Block的抽象即Buffer. 所有的讀操作會由Channel將資料讀入Buffer, 然後使用者來處理Buffer, 所有的寫操作需要先將資料填到Buffer中, 再由Channel來消費Buffer中的資料. NIO的第三個核心概念是Selector, 它是一個事件監控器, 我們將它註冊我們所感興趣的IO事件, 並且對其進行Polling, 來確定事件是否發生, 發生則做相應的IO操作. 其中, Selector所監控的物件是Channel, 我們在Selector上宣告我們關心哪一個Channel的什麼事件, Selector會監控這些Channels, 並在事件發生時通知我們.

現在, 考慮三個問題:

為什麼要引入Channel, 直接擴充套件已有的Stream類不行嗎?: 流的抽象已經很完備了, 新增更多的特性與概念只會將流的概念進一步複雜化, API更加難以使用, 這是一種很不好的API設計方式. 因此, NIO引入了一套新的抽象. Do one thing, and do it well.

為什麼引入Buffer? 直接用byte陣列可以嗎?: 實際上肯定是可以的, 但Buffer類提供了更加方便的操作. 同時, Buffer提供了很多效能上的優化.

為什麼引入Buffer? 直接讀寫byte不行嗎?: 如果直接操作byte, 效能會很低, 實際上還是需要buffering來提供效能, 與其加一層buffering抽象, 不如直接給使用者提供Buffer. 最重要的是, 基於Buffer的IO操作, 某些情況下可以直接對映成系統呼叫, 效能極高!

NIO支援阻塞與非阻塞兩種模式. 阻塞模式下, 實際上與流IO差不多, 非阻塞模式下, Channels與Selector配合, 才是它最大的威力所在.

我們可以大體將Channel分成兩類, 一種是支援SelectableChannel(除了FileChannel以外都是, 一般是網路相關的操作.), 另一種與non-SelectableChannel(即FileChannel). 前者可以與Selector一起使用, 提供強伸縮性的IO.

IO VS NIO

考慮IO與NIO的區別. 除了在概念模型的差別, IO與NIO在效能上也會有很大差異. 我們從三個方面來考慮效能問題:

可伸縮性: 流IO的在IO物件數較少及大規模IO的情況下, 表現得很好, 但是當需要處理成百上千的IO物件時, 它的效能會Drop得很快. 相反, NIO在非阻塞模式下(阻塞模式下應該與流IO具有相同的特點, 這是阻塞IO的共性), 即使用Selector, 它可以處理大量的非活躍連線, 是實現C10K的關鍵技術.

GC: 許多號稱高效能的伺服器實現, 都以Zero Allocation作為一個重要的功能點. 理想情況上, 如果沒有GC的開銷, 伺服器可以將所有時間花在有效地工作上, 並且保持一個可靠的延遲. 然後GC是不可避免的, Zero Allocation也只能是盡力而為. 而相比較而言, NIO只需要申請一個Buffer, 可以反覆使用, 而字元流在這方便表現的就比較差了, 如readLne()這類介面, 需要分配大量臨時的String物件.

API抽象層次: 相對而言, 基於Buffer的NIO抽象層次比流IO在低一些. 特別的, 系統呼叫級別的IO, 都是基於Buffer的. 當使用DirectBuffer時, 某些平臺下, OS可以直接將資料複雜到DirectBuffer中, 避免了流IO中, OS將資料複製到OS Buffer後, 又需要向JVM Heap複製地過程. Zero Copy與Zero Allocation都是高效能伺服器的重點技術. 特別的, 在使用Channel時, 需要使用DirectBuffer, 因為Channel內部使用的是DirectBuffer. 如果使用HeapBuffer, 則讀寫時, Channel會申請一個臨時的DirectBuffer, 造成效能開銷.

Memory Mapping

前面提到, FileChannel不支援非阻塞模式. 那麼, 它是不是用處不大呢? 畢竟, NIO與IO相比最大的優勢是非阻塞.

NIO中, FileChannel都一些屬於自己的特性. 即, Memory Mapping. Memory Mapping是一個比較覺見, 在此不加多說. 無論是在順序讀寫, 還是隨機讀寫中, Memory Mapping都能夠提供不弱於BufferedInputStream或者RandomAccessFile的效能.

特別強調的是, Memory Mapping可以Map的容量僅與虛擬記憶體大小有關, 與實體記憶體大小及JVM堆大小都沒有關係. 因此, 在64位平臺下, Memory Mapping可以工作得非常好.

ANIO2

聊過非阻塞IO後, 再來看看非同步IO. IO方面的概念很多, 阻塞性與非同步性是其關鍵概念. 簡單而言, 凡是需要由應用程式將資料讀寫到應用程式記憶體中的IO, 都是同步IO, 比如上面的流IO與NIO. 相對的, 凡是由OS來完成讀寫的, 就是非同步IO. 這個說法有些迷惑. 舉例而言, 在NIO中, 當應用程式檢測到某個Channel有可讀資料時, 必須顯示發起一個read請求. 而在非同步IO中, 應用程式僅僅需要告訴OS, 我需要什麼資料, 並提供給OS一個Buffer和一個回撥. OS會自己檢測Channel的可讀性, 但其發起其可讀, 會自動將資料複製到Buffer中, 並通知應用程式任務完成. 非同步IO的典型實現是NodeJS及Boost.ASIO. 顯然, 由於將任務進一步下發到了OS, 應用程式的可伸縮性及效能會大大增強. 並且, 比起非阻塞的NIO, 非同步IO程式設計更加容易一些, 效能也基本上總是優於它的.

NIO2最大的改進是引入了四個非同步Channel, 用於支援非同步讀寫. 同時, 它還增加了對檔案系統和檔案屬性的支援, 提供了WatchService/FileVisitor這些高階功能.