概覽
IO是Java中的最重要的一個部分. 其中, java.io是所有編程者都應(yīng)該掌握的IO方式. 在Java 1.4中, NIO被引入, 它引進了一種新的相對于流模型的新的IO模型, 以為非阻塞IO提供支持. 在Java 7中, NIO2又在NIO的基礎(chǔ)上, 引入了對異步IO的支持. 在這篇文章我, 我將對這幾種IO方式進行一個比較系統(tǒng)的說明及總結(jié), 同時, 分析每一種IO模型的適用范圍.
流IO
流IO是一種最為簡潔的IO方式, 幾乎所有的編程語言在其標準庫中都提供了對流IO的支持, 比如C的FILE, C++的iostream. 同樣, 在Java中, 流IO是最為基礎(chǔ)也是最為廣泛使用的IO方式, 一般來說, 大家對這種方式都比較熟悉了, 總的來說, Java提供Byte輸入流/Byte輸出流/Char輸入流/Char輸出流, 同時, 又提供了一系列的Decorator類來在基礎(chǔ)流的功能之上, 添加新的功能, 如Buffering.
用下面的四張圖可以很好地概括整個流IO的相關(guān)類.
- 輸入流
- 輸出流
- 字符輸入流
- 字符輸出流
NIO
一般來說, 流IO表現(xiàn)得很好, 對于大部分的IO場景, 它都能適應(yīng). 但是, 由于它的阻塞性, 每一個流的讀寫都需要占用一個線程. 這意味著, 流IO的可伸縮性很差. 因此, 引入非阻塞IO就再正常不過了. 實際上, NIO就是IO Multiplexing在Java中的實現(xiàn). IO Multiplexing在系統(tǒng)級語言如C/C++中應(yīng)用了很長時間. 使用IO Multiplexing, IO的伸縮性大大提高, 使用單個線程, 就可以處理大量的IO對象.
在介紹NIO的非阻塞IO之前, 先大致了解一下NIO提供的IO模型. NIO的概念概念有三個, Buffers/Channels/Selectors. 其中, Channels是輸入/輸出的管道, 所有的讀寫操作都需要通過它來完成. Channel讀寫的粒度是Block, 而不是像流IO一樣, 提供一個字節(jié)流或者字符流的抽象. 這個Block的抽象即Buffer. 所有的讀操作會由Channel將數(shù)據(jù)讀入Buffer, 然后用戶來處理Buffer, 所有的寫操作需要先將數(shù)據(jù)填到Buffer中, 再由Channel來消費Buffer中的數(shù)據(jù). NIO的第三個核心概念是Selector, 它是一個事件監(jiān)控器, 我們將它注冊我們所感興趣的IO事件, 并且對其進行Polling, 來確定事件是否發(fā)生, 發(fā)生則做相應(yīng)的IO操作. 其中, Selector所監(jiān)控的對象是Channel, 我們在Selector上聲明我們關(guān)心哪一個Channel的什么事件, Selector會監(jiān)控這些Channels, 并在事件發(fā)生時通知我們.
現(xiàn)在, 考慮三個問題:
為什么要引入Channel, 直接擴展已有的Stream類不行嗎?: 流的抽象已經(jīng)很完備了, 添加更多的特性與概念只會將流的概念進一步復雜化, API更加難以使用, 這是一種很不好的API設(shè)計方式. 因此, NIO引入了一套新的抽象. Do one thing, and do it well.
為什么引入Buffer? 直接用byte數(shù)組可以嗎?: 實際上肯定是可以的, 但Buffer類提供了更加方便的操作. 同時, Buffer提供了很多性能上的優(yōu)化.
為什么引入Buffer? 直接讀寫byte不行嗎?: 如果直接操作byte, 性能會很低, 實際上還是需要buffering來提供性能, 與其加一層buffering抽象, 不如直接給用戶提供Buffer. 最重要的是, 基于Buffer的IO操作, 某些情況下可以直接映射成系統(tǒng)調(diào)用, 性能極高!
NIO支持阻塞與非阻塞兩種模式. 阻塞模式下, 實際上與流IO差不多, 非阻塞模式下, Channels與Selector配合, 才是它最大的威力所在.
我們可以大體將Channel分成兩類, 一種是支持SelectableChannel(除了FileChannel以外都是, 一般是網(wǎng)絡(luò)相關(guān)的操作.), 另一種與non-SelectableChannel(即FileChannel). 前者可以與Selector一起使用, 提供強伸縮性的IO.
IO VS NIO
考慮IO與NIO的區(qū)別. 除了在概念模型的差別, IO與NIO在性能上也會有很大差異. 我們從三個方面來考慮性能問題:
可伸縮性: 流IO的在IO對象數(shù)較少及大規(guī)模IO的情況下, 表現(xiàn)得很好, 但是當需要處理成百上千的IO對象時, 它的性能會Drop得很快. 相反, NIO在非阻塞模式下(阻塞模式下應(yīng)該與流IO具有相同的特點, 這是阻塞IO的共性), 即使用Selector, 它可以處理大量的非活躍連接, 是實現(xiàn)C10K的關(guān)鍵技術(shù).
GC: 許多號稱高性能的服務(wù)器實現(xiàn), 都以Zero Allocation作為一個重要的功能點. 理想情況上, 如果沒有GC的開銷, 服務(wù)器可以將所有時間花在有效地工作上, 并且保持一個可靠的延遲. 然后GC是不可避免的, Zero Allocation也只能是盡力而為. 而相比較而言, NIO只需要申請一個Buffer, 可以反復使用, 而字符流在這方便表現(xiàn)的就比較差了, 如readLne()這類接口, 需要分配大量臨時的String對象.
API抽象層次: 相對而言, 基于Buffer的NIO抽象層次比流IO在低一些. 特別的, 系統(tǒng)調(diào)用級別的IO, 都是基于Buffer的. 當使用DirectBuffer時, 某些平臺下, OS可以直接將數(shù)據(jù)復雜到DirectBuffer中, 避免了流IO中, OS將數(shù)據(jù)復制到OS Buffer后, 又需要向JVM Heap復制地過程. Zero Copy與Zero Allocation都是高性能服務(wù)器的重點技術(shù). 特別的, 在使用Channel時, 需要使用DirectBuffer, 因為Channel內(nèi)部使用的是DirectBuffer. 如果使用HeapBuffer, 則讀寫時, Channel會申請一個臨時的DirectBuffer, 造成性能開銷.
Memory Mapping
前面提到, FileChannel不支持非阻塞模式. 那么, 它是不是用處不大呢? 畢竟, NIO與IO相比最大的優(yōu)勢是非阻塞.
NIO中, FileChannel都一些屬于自己的特性. 即, Memory Mapping. Memory Mapping是一個比較覺見, 在此不加多說. 無論是在順序讀寫, 還是隨機讀寫中, Memory Mapping都能夠提供不弱于BufferedInputStream或者RandomAccessFile的性能.
特別強調(diào)的是, Memory Mapping可以Map的容量僅與虛擬內(nèi)存大小有關(guān), 與物理內(nèi)存大小及JVM堆大小都沒有關(guān)系. 因此, 在64位平臺下, Memory Mapping可以工作得非常好.
NIO2
聊過非阻塞IO后, 再來看看異步IO. IO方面的概念很多, 阻塞性與異步性是其關(guān)鍵概念. 簡單而言, 凡是需要由應(yīng)用程序?qū)?shù)據(jù)讀寫到應(yīng)用程序內(nèi)存中的IO, 都是同步IO, 比如上面的流IO與NIO. 相對的, 凡是由OS來完成讀寫的, 就是異步IO. 這個說法有些迷惑. 舉例而言, 在NIO中, 當應(yīng)用程序檢測到某個Channel有可讀數(shù)據(jù)時, 必須顯示發(fā)起一個read請求. 而在異步IO中, 應(yīng)用程序僅僅需要告訴OS, 我需要什么數(shù)據(jù), 并提供給OS一個Buffer和一個回調(diào). OS會自己檢測Channel的可讀性, 但其發(fā)起其可讀, 會自動將數(shù)據(jù)復制到Buffer中, 并通知應(yīng)用程序任務(wù)完成. 異步IO的典型實現(xiàn)是NodeJS及Boost.ASIO. 顯然, 由于將任務(wù)進一步下發(fā)到了OS, 應(yīng)用程序的可伸縮性及性能會大大增強. 并且, 比起非阻塞的NIO, 異步IO編程更加容易一些, 性能也基本上總是優(yōu)于它的.
NIO2最大的改進是引入了四個異步Channel, 用于支持異步讀寫. 同時, 它還增加了對文件系統(tǒng)和文件屬性的支持, 提供了WatchService/FileVisitor這些高級功能.



