NIO 入門

在開始以前

關於本教程

新的輸入/輸出 (NIO) 庫是在 JDK 1.4 中引入的。NIO 彌補了原來的 I/O 的不足,它在標準 Java 代碼中提供了高速的、面向塊的 I/O。經過定義包含數據的類,以及經過以塊的形式處理這些數據,NIO 不用使用本機代碼就能夠利用低級優化,這是原來的 I/O 包所沒法作到的。 html

在本教程中,咱們將討論 NIO 庫的幾乎全部方面,從高級的概念性內容到底層的編程細節。除了學習諸如緩衝區和通道這樣的關鍵 I/O 元素外,您還有機會看到在更新後的庫中標準 I/O 是如何工做的。您還會了解只能經過 NIO 來完成的工做,如異步 I/O 和直接緩衝區。 java

在本教程中,咱們將使用展現 NIO 庫的不一樣方面的代碼示例。幾乎每個代碼示例都是一個大的 Java 程序的一部分,您能夠在 參考資料 中找到這個 Java 程序。在作這些練習時,咱們推薦您在本身的系統上下載、編譯和運行這些程序。在您學習了本教程之後,這些代碼將爲您的 NIO 編程努力提供一個起點。程序員

本教程是爲但願學習更多關於 JDK 1.4 NIO 庫的知識的全部程序員而寫的。爲了最大程度地從這裏的討論中獲益,您應該理解基本的 Java 編程概念,如類、繼承和使用包。多少熟悉一些原來的 I/O 庫(來自 java.io.* 包)也會有所幫助。 shell

雖然本教程要求掌握 Java 語言的工做詞彙和概念,可是不須要有不少實際編程經驗。除了完全介紹與本教程有關的全部概念外,我還保持代碼示例儘量短小和簡單。目的是讓即便沒有多少 Java 編程經驗的讀者也能容易地開始學習 NIO。 編程

如何運行代碼

源代碼歸檔文件(在 參考資料 中提供)包含了本教程中使用的全部程序。每個程序都由一個 Java 文件構成。每個文件都根據名稱來識別,而且能夠容易地與它所展現的編程概念相關聯。 api

教程中的一些程序須要命令行參數才能運行。要從命令行運行一個程序,只需使用最方便的命令行提示符。在 Windows 中,命令行提供符是 「Command」 或者 「command.com」 程序。在 UNIX 中,可使用任何 shell。 數組

須要安裝 JDK 1.4 並將它包括在路徑中,才能完成本教程中的練習。若是須要安裝和配置 JDK 1.4 的幫助,請參見 參考資料安全

輸入/輸出:概念性描述

I/O 簡介

I/O ? 或者輸入/輸出 ? 指的是計算機與外部世界或者一個程序與計算機的其他部分的之間的接口。它對於任何計算機系統都很是關鍵,於是全部 I/O 的主體其實是內置在操做系統中的。單獨的程序通常是讓系統爲它們完成大部分的工做。 服務器

在 Java 編程中,直到最近一直使用 的方式完成 I/O。全部 I/O 都被視爲單個的字節的移動,經過一個稱爲 Stream 的對象一次移動一個字節。流 I/O 用於與外部世界接觸。它也在內部使用,用於將對象轉換爲字節,而後再轉換回對象。 網絡

NIO 與原來的 I/O 有一樣的做用和目的,可是它使用不一樣的方式? 塊 I/O。正如您將在本教程中學到的,塊 I/O 的效率能夠比流 I/O 高許多。

爲何要使用 NIO?

NIO 的建立目的是爲了讓 Java 程序員能夠實現高速 I/O 而無需編寫自定義的本機代碼。NIO 將最耗時的 I/O 操做(即填充和提取緩衝區)轉移回操做系統,於是能夠極大地提升速度。

流與塊的比較

原來的 I/O 庫(在 java.io.*中) 與 NIO 最重要的區別是數據打包和傳輸的方式。正如前面提到的,原來的 I/O 以流的方式處理數據,而 NIO 以塊的方式處理數據。

面向流 的 I/O 系統一次一個字節地處理數據。一個輸入流產生一個字節的數據,一個輸出流消費一個字節的數據。爲流式數據建立過濾器很是容易。連接幾個過濾器,以便每一個過濾器只負責單個複雜處理機制的一部分,這樣也是相對簡單的。不利的一面是,面向流的 I/O 一般至關慢。

一個 面向塊 的 I/O 系統以塊的形式處理數據。每個操做都在一步中產生或者消費一個數據塊。按塊處理數據比按(流式的)字節處理數據要快得多。可是面向塊的 I/O 缺乏一些面向流的 I/O 所具備的優雅性和簡單性。

集成的 I/O

在 JDK 1.4 中原來的 I/O 包和 NIO 已經很好地集成了。 java.io.* 已經以 NIO 爲基礎從新實現了,因此如今它能夠利用 NIO 的一些特性。例如, java.io.* 包中的一些類包含以塊的形式讀寫數據的方法,這使得即便在更面向流的系統中,處理速度也會更快。

也能夠用 NIO 庫實現標準 I/O 功能。例如,能夠容易地使用塊 I/O 一次一個字節地移動數據。可是正如您會看到的,NIO 還提供了原 I/O 包中所沒有的許多好處。

通道和緩衝區

概述

通道 緩衝區 是 NIO 中的核心對象,幾乎在每個 I/O 操做中都要使用它們。

通道是對原 I/O 包中的流的模擬。到任何目的地(或來自任何地方)的全部數據都必須經過一個 Channel 對象。一個 Buffer 實質上是一個容器對象。發送給一個通道的全部對象都必須首先放到緩衝區中;一樣地,從通道中讀取的任何數據都要讀到緩衝區中。

在本節中,您會了解到 NIO 中通道和緩衝區是如何工做的。

什麼是緩衝區?

Buffer 是一個對象, 它包含一些要寫入或者剛讀出的數據。 在 NIO 中加入 Buffer 對象,體現了新庫與原 I/O 的一個重要區別。在面向流的 I/O 中,您將數據直接寫入或者將數據直接讀到 Stream 對象中。

在 NIO 庫中,全部數據都是用緩衝區處理的。在讀取數據時,它是直接讀到緩衝區中的。在寫入數據時,它是寫入到緩衝區中的。任什麼時候候訪問 NIO 中的數據,您都是將它放到緩衝區中。

緩衝區實質上是一個數組。一般它是一個字節數組,可是也可使用其餘種類的數組。可是一個緩衝區不 僅僅 是一個數組。緩衝區提供了對數據的結構化訪問,並且還能夠跟蹤系統的讀/寫進程。

緩衝區類型

最經常使用的緩衝區類型是 ByteBuffer。一個 ByteBuffer 能夠在其底層字節數組上進行 get/set 操做(即字節的獲取和設置)。

ByteBuffer 不是 NIO 中惟一的緩衝區類型。事實上,對於每一種基本 Java 類型都有一種緩衝區類型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

每個 Buffer 類都是 Buffer 接口的一個實例。 除了 ByteBuffer,每個 Buffer 類都有徹底同樣的操做,只是它們所處理的數據類型不同。由於大多數標準 I/O 操做都使用 ByteBuffer,因此它具備全部共享的緩衝區操做以及一些特有的操做。

如今您能夠花一點時間運行 UseFloatBuffer.java,它包含了類型化的緩衝區的一個應用例子。

什麼是通道?

Channel是一個對象,能夠經過它讀取和寫入數據。拿 NIO 與原來的 I/O 作個比較,通道就像是流。

正如前面提到的,全部數據都經過 Buffer 對象來處理。您永遠不會將字節直接寫入通道中,相反,您是將數據寫入包含一個或者多個字節的緩衝區。一樣,您不會直接從通道中讀取字節,而是將數據從通道讀入緩衝區,再從緩衝區獲取這個字節。

通道類型

通道與流的不一樣之處在於通道是雙向的。而流只是在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類), 而 通道 能夠用於讀、寫或者同時用於讀寫。

由於它們是雙向的,因此通道能夠比流更好地反映底層操做系統的真實狀況。特別是在 UNIX 模型中,底層操做系統通道是雙向的。

從理論到實踐:NIO 中的讀和寫

概述

讀和寫是 I/O 的基本過程。從一個通道中讀取很簡單:只需建立一個緩衝區,而後讓通道將數據讀到這個緩衝區中。寫入也至關簡單:建立一個緩衝區,用數據填充它,而後讓通道用這些數據來執行寫入操做。

在本節中,咱們將學習有關在 Java 程序中讀取和寫入數據的一些知識。咱們將回顧 NIO 的主要組件(緩衝區、通道和一些相關的方法),看看它們是如何交互以進行讀寫的。在接下來的幾節中,咱們將更詳細地分析這其中的每一個組件以及其交互。

從文件中讀取

在咱們第一個練習中,咱們將從一個文件中讀取一些數據。若是使用原來的 I/O,那麼咱們只需建立一個 FileInputStream 並從它那裏讀取。而在 NIO 中,狀況稍有不一樣:咱們首先從 FileInputStream 獲取一個 Channel 對象,而後使用這個通道來讀取數據。

在 NIO 系統中,任什麼時候候執行一個讀操做,您都是從通道中讀取,可是您不是 直接 從通道讀取。由於全部數據最終都駐留在緩衝區中,因此您是從通道讀到緩衝區中。

所以讀取文件涉及三個步驟:(1) 從 FileInputStream 獲取 Channel,(2) 建立 Buffer,(3) 將數據從 Channel 讀到 Buffer 中。

如今,讓咱們看一下這個過程。

三個容易的步驟

第一步是獲取通道。咱們從 FileInputStream 獲取通道:

FileInputStream fin = new FileInputStream( "readandshow.txt" );
FileChannel fc = fin.getChannel();

下一步是建立緩衝區:

ByteBuffer buffer = ByteBuffer.allocate( 1024 );

最後,須要將數據從通道讀到緩衝區中,以下所示:

fc.read( buffer );

您會注意到,咱們不須要告訴通道要讀 多少數據
到緩衝區中。每個緩衝區都有複雜的內部統計機制,它會跟蹤已經讀了多少數據以及還有多少空間能夠容納更多的數據。咱們將在 緩衝區內部細節 中介紹更多關於緩衝區統計機制的內容。

寫入文件

在 NIO 中寫入文件相似於從文件中讀取。首先從 FileOutputStream 獲取一個通道:

FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
FileChannel fc = fout.getChannel();

下一步是建立一個緩衝區並在其中放入一些數據 - 在這裏,數據將從一個名爲 message 的數組中取出,這個數組包含字符串
"Some bytes" 的 ASCII 字節(本教程後面將會解釋 buffer.flip()
buffer.put() 調用)。

ByteBuffer buffer = ByteBuffer.allocate( 1024 );

for (int i=0; i<message.length; ++i) {
buffer.put( message[i] );
}
buffer.flip();

最後一步是寫入緩衝區中:

fc.write( buffer );

注意在這裏一樣不須要告訴通道要寫入多數據。緩衝區的內部統計機制會跟蹤它包含多少數據以及還有多少數據要寫入。

讀寫結合

下面咱們將看一下在結合讀和寫時會有什麼狀況。咱們以一個名爲 CopyFile.java
的簡單程序做爲這個練習的基礎,它將一個文件的全部內容拷貝到另外一個文件中。CopyFile.java 執行三個基本操做:首先建立一個
Buffer,而後從源文件中將數據讀到這個緩衝區中,而後將緩衝區寫入目標文件。這個程序不斷重複 ― 讀、寫、讀、寫 ―
直到源文件結束。

CopyFile 程序讓您看到咱們如何檢查操做的狀態,以及如何使用 clear()
flip() 方法重設緩衝區,並準備緩衝區以便將新讀取的數據寫到另外一個通道中。

運行 CopyFile 例子

由於緩衝區會跟蹤它本身的數據,因此 CopyFile 程序的內部循環 (inner loop) 很是簡單,以下所示:

fcin.read( buffer );
fcout.write( buffer );

第一行將數據從輸入通道 fcin 中讀入緩衝區,第二行將這些數據寫到輸出通道 fcout

檢查狀態

下一步是檢查拷貝什麼時候完成。當沒有更多的數據時,拷貝就算完成,而且能夠在 read() 方法返回 -1
是判斷這一點,以下所示:

int r = fcin.read( buffer );

if (r==-1) {
break;
}

重設緩衝區

最後,在從輸入通道讀入緩衝區以前,咱們調用 clear() 方法。一樣,在將緩衝區寫入輸出通道以前,咱們調用
flip() 方法,以下所示:

buffer.clear();
int r = fcin.read( buffer );

if (r==-1) {
break;
}

buffer.flip();
fcout.write( buffer );

clear() 方法重設緩衝區,使它能夠接受讀入的數據。 flip()
方法讓緩衝區能夠將新讀入的數據寫入另外一個通道。

緩衝區內部細節

概述

本節將介紹 NIO 中兩個重要的緩衝區組件:狀態變量和訪問方法 (accessor)。

狀態變量是前一節中提到的"內部統計機制"的關鍵。每個讀/寫操做都會改變緩衝區的狀態。經過記錄和跟蹤這些變化,緩衝區就可可以內部地管理本身的資源。

在從通道讀取數據時,數據被放入到緩衝區。在有些狀況下,能夠將這個緩衝區直接寫入另外一個通道,可是在通常狀況下,您還須要查看數據。這是使用
訪問方法 get() 來完成的。一樣,若是要將原始數據放入緩衝區中,就要使用訪問方法
put()

在本節中,您將學習關於 NIO 中的狀態變量和訪問方法的內容。咱們將描述每個組件,並讓您有機會看到它的實際應用。雖然 NIO
的內部統計機制初看起來可能很複雜,可是您很快就會看到大部分的實際工做都已經替您完成了。您可能習慣於經過手工編碼進行簿記 ―
即便用字節數組和索引變量,如今它已在 NIO 中內部地處理了。

狀態變量

能夠用三個值指定緩衝區在任意時刻的狀態:

  • position
  • limit
  • capacity

這三個變量一塊兒能夠跟蹤緩衝區的狀態和它所包含的數據。咱們將在下面的小節中詳細分析每個變量,還要介紹它們如何適應典型的讀/寫(輸入/輸出)進程。在這個例子中,咱們假定要將數據從一個輸入通道拷貝到一個輸出通道。

Position

您能夠回想一下,緩衝區實際上就是美化了的數組。在從通道讀取時,您將所讀取的數據放到底層的數組中。 position
變量跟蹤已經寫了多少數據。更準確地說,它指定了下一個字節將放到數組的哪個元素中。所以,若是您從通道中讀三個字節到緩衝區中,那麼緩衝區的
position 將會設置爲3,指向數組中第四個元素。

一樣,在寫入通道時,您是從緩衝區中獲取數據。 position
值跟蹤從緩衝區中獲取了多少數據。更準確地說,它指定下一個字節來自數組的哪個元素。所以若是從緩衝區寫了5個字節到通道中,那麼緩衝區的
position 將被設置爲5,指向數組的第六個元素。

Limit

limit 變量代表還有多少數據須要取出(在從緩衝區寫入通道時),或者還有多少空間能夠放入數據(在從通道讀入緩衝區時)。

position 老是小於或者等於 limit

Capacity

緩衝區的 capacity 代表能夠儲存在緩衝區中的最大數據容量。實際上,它指定了底層數組的大小 ―
或者至少是指定了准許咱們使用的底層數組的容量。

limit 決不能大於 capacity

觀察變量

咱們首先觀察一個新建立的緩衝區。出於本例子的須要,咱們假設這個緩衝區的 總容量 爲8個字節。
Buffer 的狀態以下所示:

Buffer state
Buffer state

回想一下 ,limit 決不能大於 capacity,此例中這兩個值都被設置爲
8。咱們經過將它們指向數組的尾部以後(若是有第8個槽,則是第8個槽所在的位置)來講明這點。

Array
Array

position 設置爲0。若是咱們讀一些數據到緩衝區中,那麼下一個讀取的數據就進入 slot 0
。若是咱們從緩衝區寫一些數據,從緩衝區讀取的下一個字節就來自 slot 0 。 position 設置以下所示:

Position setting
Position setting

因爲 capacity 不會改變,因此咱們在下面的討論中能夠忽略它。

第一次讀取

如今咱們能夠開始在新建立的緩衝區上進行讀/寫操做。首先從輸入通道中讀一些數據到緩衝區中。第一次讀取獲得三個字節。它們被放到數組中從
position 開始的位置,這時 position 被設置爲 0。讀完以後,position 就增長到 3,以下所示:

Position increased to 3
Position increased to 3

limit 沒有改變。

第二次讀取

在第二次讀取時,咱們從輸入通道讀取另外兩個字節到緩衝區中。這兩個字節儲存在由 position 所指定的位置上,
position 於是增長 2:

Position increased by 2
Position increased by 2

limit 沒有改變。

flip

如今咱們要將數據寫到輸出通道中。在這以前,咱們必須調用 flip() 方法。這個方法作兩件很是重要的事:

  1. 它將 limit 設置爲當前 position
  2. 它將 position 設置爲 0。

前一小節中的圖顯示了在 flip 以前緩衝區的狀況。下面是在 flip 以後的緩衝區:

Buffer after the flip
Buffer after the flip

咱們如今能夠將數據從緩衝區寫入通道了。 position 被設置爲 0,這意味着咱們獲得的下一個字節是第一個字節。
limit 已被設置爲原來的
position,這意味着它包括之前讀到的全部字節,而且一個字節也很少。

第一次寫入

在第一次寫入時,咱們從緩衝區中取四個字節並將它們寫入輸出通道。這使得 position 增長到 4,而
limit 不變,以下所示:

Position advanced to 4, limit unchanged
Position advanced to 4, limit unchanged

第二次寫入

咱們只剩下一個字節可寫了。 limit在咱們調用 flip() 時被設置爲 5,而且
position 不能超過
limit。因此最後一次寫入操做從緩衝區取出一個字節並將它寫入輸出通道。這使得 position
增長到 5,並保持 limit 不變,以下所示:

Position advanced to 5, limit unchanged
Position advanced to 5, limit unchanged

clear

最後一步是調用緩衝區的 clear() 方法。這個方法重設緩衝區以便接收更多的字節。 Clear
作兩種很是重要的事情:

  1. 它將 limit 設置爲與 capacity 相同。
  2. 它設置 position 爲 0。

下圖顯示了在調用 clear() 後緩衝區的狀態:

State of the buffer after clear() has been called
State of the buffer after clear() has been called

緩衝區如今能夠接收新的數據了。

訪問方法

到目前爲止,咱們只是使用緩衝區將數據從一個通道轉移到另外一個通道。然而,程序常常須要直接處理數據。例如,您可能須要將用戶數據保存到磁盤。在這種狀況下,您必須將這些數據直接放入緩衝區,而後用通道將緩衝區寫入磁盤。

或者,您可能想要從磁盤讀取用戶數據。在這種狀況下,您要將數據從通道讀到緩衝區中,而後檢查緩衝區中的數據。

在本節的最後,咱們將詳細分析如何使用 ByteBuffer 類的 get()
put() 方法直接訪問緩衝區中的數據。

get() 方法

ByteBuffer 類中有四個 get() 方法:

  1. byte get();
  2. ByteBuffer get( byte dst[] );
  3. ByteBuffer get( byte dst[], int offset, int length );
  4. byte get( int index );

第一個方法獲取單個字節。第二和第三個方法將一組字節讀到一個數組中。第四個方法從緩衝區中的特定位置獲取字節。那些返回
ByteBuffer 的方法只是返回調用它們的緩衝區的 this 值。

此外,咱們認爲前三個 get() 方法是相對的,而最後一個方法是絕對的。 相對 意味着
get() 操做服從 limitposition 值 ―
更明確地說,字節是從當前 position 讀取的,而 position
get 以後會增長。另外一方面,一個 絕對 方法會忽略 limit
position 值,也不會影響它們。事實上,它徹底繞過了緩衝區的統計方法。

上面列出的方法對應於 ByteBuffer 類。其餘類有等價的 get()
方法,這些方法除了不是處理字節外,其它方面是是徹底同樣的,它們處理的是與該緩衝區類相適應的類型。

put()方法

ByteBuffer 類中有五個 put() 方法:

  1. ByteBuffer put( byte b );
  2. ByteBuffer put( byte src[] );
  3. ByteBuffer put( byte src[], int offset, int length );
  4. ByteBuffer put( ByteBuffer src );
  5. ByteBuffer put( int index, byte b );

第一個方法 寫入(put) 單個字節。第二和第三個方法寫入來自一個數組的一組字節。第四個方法將數據從一個給定的源
ByteBuffer 寫入這個 ByteBuffer。第五個方法將字節寫入緩衝區中特定的
位置 。那些返回 ByteBuffer 的方法只是返回調用它們的緩衝區的
this 值。

get() 方法同樣,咱們將把 put() 方法劃分爲 相對 或者
絕對 的。前四個方法是相對的,而第五個方法是絕對的。

上面顯示的方法對應於 ByteBuffer 類。其餘類有等價的 put()
方法,這些方法除了不是處理字節以外,其它方面是徹底同樣的。它們處理的是與該緩衝區類相適應的類型。

類型化的 get() 和 put() 方法

除了前些小節中描述的 get()put() 方法,
ByteBuffer 還有用於讀寫不一樣類型的值的其餘方法,以下所示:

  • getByte()
  • getChar()
  • getShort()
  • getInt()
  • getLong()
  • getFloat()
  • getDouble()
  • putByte()
  • putChar()
  • putShort()
  • putInt()
  • putLong()
  • putFloat()
  • putDouble()

事實上,這其中的每一個方法都有兩種類型 ― 一種是相對的,另外一種是絕對的。它們對於讀取格式化的二進制數據(如圖像文件的頭部)頗有用。

您能夠在例子程序 TypesInByteBuffer.java 中看到這些方法的實際應用。

緩衝區的使用:一個內部循環

下面的內部循環歸納了使用緩衝區將數據從輸入通道拷貝到輸出通道的過程。

while (true) {
buffer.clear();
int r = fcin.read( buffer );


}

if (r==-1) { break; } buffer.flip(); fcout.write( buffer );

read()write() 調用獲得了極大的簡化,由於許多工做細節都由緩衝區完成了。
clear()flip() 方法用於讓緩衝區在讀和寫之間切換。

關於緩衝區的更多內容

概述

到目前爲止,您已經學習了使用緩衝區進行平常工做所須要掌握的大部份內容。咱們的例子沒怎麼超出標準的讀/寫過程種類,在原來的 I/O 中能夠像在 NIO
中同樣容易地實現這樣的標準讀寫過程。

本節將討論使用緩衝區的一些更復雜的方面,好比緩衝區分配、包裝和分片。咱們還會討論 NIO 帶給 Java
平臺的一些新功能。您將學到如何建立不一樣類型的緩衝區以達到不一樣的目的,如可保護數據不被修改的 只讀
緩衝區,和直接映射到底層操做系統緩衝區的 直接 緩衝區。咱們將在本節的最後介紹如何在 NIO 中建立內存映射文件。

緩衝區分配和包裝

在可以讀和寫以前,必須有一個緩衝區。要建立緩衝區,您必須 分配 它。咱們使用靜態方法 allocate()
來分配緩衝區:

ByteBuffer buffer = ByteBuffer.allocate( 1024 );

allocate() 方法分配一個具備指定大小的底層數組,並將它包裝到一個緩衝區對象中 ― 在本例中是一個
ByteBuffer

您還能夠將一個現有的數組轉換爲緩衝區,以下所示:

byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap( array );

本例使用了 wrap()
方法將一個數組包裝爲緩衝區。必須很是當心地進行這類操做。一旦完成包裝,底層數據就能夠經過緩衝區或者直接訪問。

緩衝區分片

slice() 方法根據現有的緩衝區建立一種 子緩衝區
。也就是說,它建立一個新的緩衝區,新緩衝區與原來的緩衝區的一部分共享數據。

使用例子能夠最好地說明這點。讓咱們首先建立一個長度爲 10 的 ByteBuffer

ByteBuffer buffer = ByteBuffer.allocate( 10 );

而後使用數據來填充這個緩衝區,在第 n 個槽中放入數字 n

for (int i=0; i<buffer.capacity(); ++i) {
buffer.put( (byte)i );
}

如今咱們對這個緩衝區 分片 ,以建立一個包含槽 3 到槽 6 的子緩衝區。在某種意義上,子緩衝區就像原來的緩衝區中的一個
窗口

窗口的起始和結束位置經過設置 positionlimit 值來指定,而後調用
Bufferslice() 方法:

buffer.position( 3 );
buffer.limit( 7 );
ByteBuffer slice = buffer.slice();

是緩衝區的 子緩衝區 。不過, 片斷
緩衝區 共享同一個底層數據數組,咱們在下一節將會看到這一點。

緩衝區份片和數據共享

咱們已經建立了原緩衝區的子緩衝區,而且咱們知道緩衝區和子緩衝區共享同一個底層數據數組。讓咱們看看這意味着什麼。

咱們遍歷子緩衝區,將每個元素乘以 11 來改變它。例如,5 會變成 55。

for (int i=0; i<slice.capacity(); ++i) {
byte b = slice.get( i );
b *= 11;
slice.put( i, b );
}

最後,再看一下原緩衝區中的內容:

buffer.position( 0 );
buffer.limit( buffer.capacity() );

while (buffer.remaining()>0) {
System.out.println( buffer.get() );
}

結果代表只有在子緩衝區窗口中的元素被改變了:

$ java SliceBuffer
0
1
2
33
44
55
66
7
8
9

緩衝區片對於促進抽象很是有幫助。能夠編寫本身的函數處理整個緩衝區,並且若是想要將這個過程應用於子緩衝區上,您只需取主緩衝區的一個片,並將它傳遞給您的函數。這比編寫本身的函數來取額外的參數以指定要對緩衝區的哪一部分進行操做更容易。

只讀緩衝區

只讀緩衝區很是簡單 ― 您能夠讀取它們,可是不能向它們寫入。能夠經過調用緩衝區的 asReadOnlyBuffer()
方法,將任何常規緩衝區轉換爲只讀緩衝區,這個方法返回一個與原緩衝區徹底相同的緩衝區(並與其共享數據),只不過它是隻讀的。

只讀緩衝區對於保護數據頗有用。在將緩衝區傳遞給某個對象的方法時,您沒法知道這個方法是否會修改緩衝區中的數據。建立一個只讀的緩衝區能夠 保證
該緩衝區不會被修改。

不能將只讀的緩衝區轉換爲可寫的緩衝區。

直接和間接緩衝區

另外一種有用的 ByteBuffer 是直接緩衝區。 直接緩衝區 是爲加快 I/O
速度,而以一種特殊的方式分配其內存的緩衝區。

實際上,直接緩衝區的準肯定義是與實現相關的。Sun 的文檔是這樣描述直接緩衝區的:

給定一個直接字節緩衝區,Java 虛擬機將盡最大努力直接對它執行本機 I/O 操做。也就是說,它會在每一次調用底層操做系統的本機 I/O
操做以前(或以後),嘗試避免將緩衝區的內容拷貝到一箇中間緩衝區中(或者從一箇中間緩衝區中拷貝數據)。

您能夠在例子程序 FastCopyFile.java 中看到直接緩衝區的實際應用,這個程序是 CopyFile.java
的另外一個版本,它使用了直接緩衝區以提升速度。

還能夠用內存映射文件建立直接緩衝區。

內存映射文件 I/O

內存映射文件 I/O 是一種讀和寫文件數據的方法,它能夠比常規的基於流或者基於通道的 I/O 快得多。

內存映射文件 I/O
是經過使文件中的數據神奇般地出現爲內存數組的內容來完成的。這其初聽起來彷佛不過就是將整個文件讀到內存中,可是事實上並非這樣。通常來講,只有文件中實際讀取或者寫入的部分纔會送入(或者
映射 )到內存中。

內存映射並不真的神奇或者多麼不尋常。現代操做系統通常根據須要將文件的部分映射爲內存的部分,從而實現文件系統。Java
內存映射機制不過是在底層操做系統中能夠採用這種機制時,提供了對該機制的訪問。

儘管建立內存映射文件至關簡單,可是向它寫入多是危險的。僅只是改變數組的單個元素這樣的簡單操做,就可能會直接修改磁盤上的文件。修改數據與將數據保存到磁盤是沒有分開的。

將文件映射到內存

瞭解內存映射的最好方法是使用例子。在下面的例子中,咱們要將一個 FileChannel
(它的所有或者部分)映射到內存中。爲此咱們將使用 FileChannel.map() 方法。下面代碼行將文件的前 1024
個字節映射到內存中:

MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE,
0, 1024 );

map() 方法返回一個 MappedByteBuffer,它是
ByteBuffer 的子類。所以,您能夠像使用其餘任何 ByteBuffer
同樣使用新映射的緩衝區,操做系統會在須要時負責執行行映射。

分散和彙集

概述

分散/彙集 I/O 是使用多個而不是單個緩衝區來保存數據的讀寫方法。

一個分散的讀取就像一個常規通道讀取,只不過它是將數據讀到一個緩衝區數組中而不是讀到單個緩衝區中。一樣地,一個彙集寫入是向緩衝區數組而不是向單個緩衝區寫入數據。

分散/彙集 I/O 對於將數據流劃分爲單獨的部分頗有用,這有助於實現複雜的數據格式。

分散/彙集 I/O

通道能夠有選擇地實現兩個新的接口: ScatteringByteChannel
GatheringByteChannel。一個 ScatteringByteChannel
是一個具備兩個附加讀方法的通道:

  • long read( ByteBuffer[] dsts );
  • long read( ByteBuffer[] dsts, int offset, int length );

這些 long read() 方法很像標準的 read
方法,只不過它們不是取單個緩衝區而是取一個緩衝區數組。

分散讀取 中,通道依次填充每一個緩衝區。填滿一個緩衝區後,它就開始填充下一個。在某種意義上,緩衝區數組就像一個大緩衝區。

分散/彙集的應用

分散/彙集 I/O
對於將數據劃分爲幾個部分頗有用。例如,您可能在編寫一個使用消息對象的網絡應用程序,每個消息被劃分爲固定長度的頭部和固定長度的正文。您能夠建立一個恰好能夠容納頭部的緩衝區和另外一個恰好能夠容難正文的緩衝區。當您將它們放入一個數組中並使用分散讀取來向它們讀入消息時,頭部和正文將整齊地劃分到這兩個緩衝區中。

咱們從緩衝區所獲得的方便性對於緩衝區數組一樣有效。由於每個緩衝區都跟蹤本身還能夠接受多少數據,因此分散讀取會自動找到有空間接受數據的第一個緩衝區。在這個緩衝區填滿後,它就會移動到下一個緩衝區。

彙集寫入

彙集寫入 相似於分散讀取,只不過是用來寫入。它也有接受緩衝區數組的方法:

  • long write( ByteBuffer[] srcs );
  • long write( ByteBuffer[] srcs, int offset, int length );

彙集寫對於把一組單獨的緩衝區中組成單個數據流頗有用。爲了與上面的消息例子保持一致,您可使用匯集寫入來自動將網絡消息的各個部分組裝爲單個數據流,以便跨越網絡傳輸消息。

從例子程序 UseScatterGather.java 中能夠看到分散讀取和彙集寫入的實際應用。

文件鎖定

概述

文件鎖定初看起來可能讓人迷惑。它 彷佛 指的是防止程序或者用戶訪問特定文件。事實上,文件鎖就像常規的 Java 對象鎖 ― 它們是
勸告式的(advisory) 鎖。它們不阻止任何形式的數據訪問,相反,它們經過鎖的共享和獲取賴容許系統的不一樣部分相互協調。

您能夠鎖定整個文件或者文件的一部分。若是您獲取一個排它鎖,那麼其餘人就不能得到同一個文件或者文件的一部分上的鎖。若是您得到一個共享鎖,那麼其餘人能夠得到同一個文件或者文件一部分上的共享鎖,可是不能得到排它鎖。文件鎖定並不老是出於保護數據的目的。例如,您可能臨時鎖定一個文件以保證特定的寫操做成爲原子的,而不會有其餘程序的干擾。

大多數操做系統提供了文件系統鎖,可是它們並不都是採用一樣的方式。有些實現提供了共享鎖,而另外一些僅提供了排它鎖。事實上,有些實現使得文件的鎖定部分不可訪問,儘管大多數實現不是這樣的。

在本節中,您將學習如何在 NIO 中執行簡單的文件鎖過程,咱們還將探討一些保證被鎖定的文件儘量可移植的方法。

鎖定文件

要獲取文件的一部分上的鎖,您要調用一個打開的 FileChannel 上的 lock()
方法。注意,若是要獲取一個排它鎖,您必須以寫方式打開文件。

RandomAccessFile raf = new RandomAccessFile( "usefilelocks.txt", "rw" );
FileChannel fc = raf.getChannel();
FileLock lock = fc.lock( start, end, false );

在擁有鎖以後,您能夠執行須要的任何敏感操做,而後再釋放鎖:

lock.release();

在釋放鎖後,嘗試得到鎖的其餘任何程序都有機會得到它。

本小節的例子程序 UseFileLocks.java
必須與它本身並行運行。這個程序獲取一個文件上的鎖,持有三秒鐘,而後釋放它。若是同時運行這個程序的多個實例,您會看到每一個實例依次得到鎖。

文件鎖定和可移植性

文件鎖定多是一個複雜的操做,特別是考慮到不一樣的操做系統是以不一樣的方式實現鎖這一事實。下面的指導原則將幫助您儘量保持代碼的可移植性:

  • 只使用排它鎖。
  • 將全部的鎖視爲勸告式的(advisory)。

連網和異步 I/O

概述

連網是學習異步 I/O 的很好基礎,而異步 I/O 對於在 Java 語言中執行任何輸入/輸出過程的人來講,無疑都是必須具有的知識。NIO 中的連網與
NIO 中的其餘任何操做沒有什麼不一樣 ― 它依賴通道和緩衝區,而您一般使用 InputStream
OutputStream 來得到通道。

本節首先介紹異步 I/O 的基礎 ― 它是什麼以及它不是什麼,而後轉向更實用的、程序性的例子。

異步 I/O

異步 I/O 是一種 沒有阻塞地 讀寫數據的方法。一般,在代碼進行 read()
調用時,代碼會阻塞直至有可供讀取的數據。一樣, write() 調用將會阻塞直至數據可以寫入。

另外一方面,異步 I/O 調用不會阻塞。相反,您將註冊對特定 I/O 事件的興趣 ―
可讀的數據的到達、新的套接字鏈接,等等,而在發生這樣的事件時,系統將會告訴您。

異步 I/O 的一個優點在於,它容許您同時根據大量的輸入和輸出執行 I/O。同步程序經常要求助於輪詢,或者建立許許多多的線程以處理大量的鏈接。使用異步
I/O,您能夠監放任何數量的通道上的事件,不用輪詢,也不用額外的線程。

咱們將經過研究一個名爲 MultiPortEcho.java 的例子程序來查看異步 I/O 的實際應用。這個程序就像傳統的 echo
server
,它接受網絡鏈接並向它們迴響它們可能發送的數據。不過它有一個附加的特性,就是它能同時監聽多個端口,並處理來自全部這些端口的鏈接。而且它只在單個線程中完成全部這些工做。

Selectors

本節的闡述對應於 MultiPortEcho 的源代碼中的 go()
方法的實現,所以應該看一下源代碼,以便對所發生的事情有個更全面的瞭解。

異步 I/O 中的核心對象名爲 SelectorSelector 就是您註冊對各類 I/O
事件的興趣的地方,並且當那些事件發生時,就是這個對象告訴您所發生的事件。

因此,咱們須要作的第一件事就是建立一個 Selector

Selector selector = Selector.open();

而後,咱們將對不一樣的通道對象調用 register() 方法,以便註冊咱們對這些對象中發生的 I/O
事件的興趣。register() 的第一個參數老是這個 Selector

打開一個 ServerSocketChannel

爲了接收鏈接,咱們須要一個 ServerSocketChannel。事實上,咱們要監聽的每個端口都須要有一個
ServerSocketChannel 。對於每個端口,咱們打開一個
ServerSocketChannel,以下所示:

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking( false );

ServerSocket ss = ssc.socket();
InetSocketAddress address = new InetSocketAddress( ports[i] );
ss.bind( address );

第一行建立一個新的 ServerSocketChannel ,最後三行將它綁定到給定的端口。第二行將
ServerSocketChannel 設置爲 非阻塞的
。咱們必須對每個要使用的套接字通道調用這個方法,不然異步 I/O 就不能工做。

選擇鍵

下一步是將新打開的 ServerSocketChannels 註冊到
Selector上。爲此咱們使用 ServerSocketChannel.register() 方法,以下所示:

SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );

register() 的第一個參數老是這個 Selector。第二個參數是
OP_ACCEPT,這裏它指定咱們想要監聽 accept
事件,也就是在新的鏈接創建時所發生的事件。這是適用於 ServerSocketChannel 的惟一事件類型。

請注意對 register() 的調用的返回值。 SelectionKey 表明這個通道在此
Selector 上的這個註冊。當某個 Selector
通知您某個傳入事件時,它是經過提供對應於該事件的 SelectionKey
來進行的。SelectionKey 還能夠用於取消通道的註冊。

內部循環

如今已經註冊了咱們對一些 I/O 事件的興趣,下面將進入主循環。使用 Selectors
的幾乎每一個程序都像下面這樣使用內部循環:

int num = selector.select();

Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();

while (it.hasNext()) {
SelectionKey key = (SelectionKey)it.next();
// ... deal with I/O event ...
}

首先,咱們調用 Selectorselect()
方法。這個方法會阻塞,直到至少有一個已註冊的事件發生。當一個或者更多的事件發生時, select()
方法將返回所發生的事件的數量。

接下來,咱們調用 SelectorselectedKeys() 方法,它返回發生了事件的
SelectionKey 對象的一個 集合

咱們經過迭代 SelectionKeys 並依次處理每一個 SelectionKey
來處理事件。對於每個 SelectionKey,您必須肯定發生的是什麼 I/O 事件,以及這個事件影響哪些 I/O
對象。

監聽新鏈接

程序執行到這裏,咱們僅註冊了 ServerSocketChannel,而且僅註冊它們「接收」事件。爲確認這一點,咱們對
SelectionKey 調用 readyOps() 方法,並檢查發生了什麼類型的事件:

if ((key.readyOps() & SelectionKey.OP_ACCEPT)
== SelectionKey.OP_ACCEPT) {


}

// Accept the new connection // ...

能夠確定地說, readOps() 方法告訴咱們該事件是新的鏈接。

接受新的鏈接

由於咱們知道這個服務器套接字上有一個傳入鏈接在等待,因此能夠安全地接受它;也就是說,不用擔憂 accept() 操做會阻塞:

ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();

下一步是將新鏈接的 SocketChannel
配置爲非阻塞的。並且因爲接受這個鏈接的目的是爲了讀取來自套接字的數據,因此咱們還必須將 SocketChannel 註冊到
Selector上,以下所示:

sc.configureBlocking( false );
SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );

注意咱們使用 register()OP_READ 參數,將
SocketChannel 註冊用於 讀取 而不是 接受 新鏈接。

刪除處理過的 SelectionKey

在處理 SelectionKey 以後,咱們幾乎能夠返回主循環了。可是咱們必須首先將處理過的
SelectionKey
從選定的鍵集合中刪除。若是咱們沒有刪除處理過的鍵,那麼它仍然會在主集合中以一個激活的鍵出現,這會致使咱們嘗試再次處理它。咱們調用迭代器的
remove() 方法來刪除處理過的 SelectionKey

it.remove();

如今咱們能夠返回主循環並接受從一個套接字中傳入的數據(或者一個傳入的 I/O 事件)了。

傳入的 I/O

當來自一個套接字的數據到達時,它會觸發一個 I/O 事件。這會致使在主循環中調用
Selector.select(),並返回一個或者多個 I/O 事件。這一次,
SelectionKey 將被標記爲 OP_READ 事件,以下所示:

} else if ((key.readyOps() & SelectionKey.OP_READ)
== SelectionKey.OP_READ) {
// Read the data
SocketChannel sc = (SocketChannel)key.channel();
// ...
}

與之前同樣,咱們取得發生 I/O 事件的通道並處理它。在本例中,因爲這是一個 echo
server,咱們只但願從套接字中讀取數據並立刻將它發送回去。關於這個過程的細節,請參見 參考資料 中的源代碼 (MultiPortEcho.java)。

回到主循環

每次返回主循環,咱們都要調用 selectSelector()方法,並取得一組
SelectionKey。每一個鍵表明一個 I/O 事件。咱們處理事件,從選定的鍵集中刪除
SelectionKey,而後返回主循環的頂部。

這個程序有點過於簡單,由於它的目的只是展現異步 I/O 所涉及的技術。在現實的應用程序中,您須要經過將通道從
Selector
中刪除來處理關閉的通道。並且您可能要使用多個線程。這個程序能夠僅使用一個線程,由於它只是一個演示,可是在現實場景中,建立一個線程池來負責 I/O
事件處理中的耗時部分會更有意義。

字符集

概述

根據 Sun 的文檔,一個 Charset 是「十六位 Unicode
字符序列與字節序列之間的一個命名的映射」。實際上,一個 Charset 容許您以儘量最具可移植性的方式讀寫字符序列。

Java 語言被定義爲基於
Unicode。然而在實際上,許多人編寫代碼時都假設一個字符在磁盤上或者在網絡流中用一個字節表示。這種假設在許多狀況下成立,可是並非在全部狀況下都成立,並且隨着計算機變得對
Unicode 愈來愈友好,這個假設就日益變得不能成立了。

在本節中,咱們將看一下如何使用 Charsets
以適合現代文本格式的方式處理文本數據。這裏將使用的示例程序至關簡單,不過,它觸及了使用 Charset
的全部關鍵方面:爲給定的字符編碼建立 Charset,以及使用該 Charset
解碼和編碼文本數據。

編碼/解碼

要讀和寫文本,咱們要分別使用 CharsetDecoder
CharsetEncoder。將它們稱爲 編碼器 解碼器 是有道理的。一個
字符 再也不表示一個特定的位模式,而是表示字符系統中的一個實體。所以,由某個實際的位模式表示的字符必須以某種特定的
編碼 來表示。

CharsetDecoder 用於將逐位表示的一串字符轉換爲具體的 char 值。一樣,一個
CharsetEncoder 用於將字符轉換回位。

在下一個小節中,咱們將考察一個使用這些對象來讀寫數據的程序。

處理文本的正確方式

如今咱們將分析這個例子程序 UseCharsets.java。這個程序很是簡單 ―
它從一個文件中讀取一些文本,並將該文本寫入另外一個文件。可是它把該數據看成文本數據,並使用 CharBuffer
來將該數句讀入一個 CharsetDecoder 中。一樣,它使用 CharsetEncoder
來寫回該數據。

咱們將假設字符以 ISO-8859-1(Latin1) 字符集(這是 ASCII 的標準擴展)的形式儲存在磁盤上。儘管咱們必須爲使用 Unicode
作好準備,可是也必須認識到不一樣的文件是以不一樣的格式儲存的,而 ASCII 無疑是很是廣泛的一種格式。事實上,每種 Java
實現都要求對如下字符編碼提供徹底的支持:

  • US-ASCII
  • ISO-8859-1
  • UTF-8
  • UTF-16BE
  • UTF-16LE
  • UTF-16

示例程序

在打開相應的文件、將輸入數據讀入名爲 inputDataByteBuffer
以後,咱們的程序必須建立 ISO-8859-1 (Latin1) 字符集的一個實例:

Charset latin1 = Charset.forName( "ISO-8859-1" );

而後,建立一個解碼器(用於讀取)和一個編碼器 (用於寫入):

CharsetDecoder decoder = latin1.newDecoder();
CharsetEncoder encoder = latin1.newEncoder();

爲了將字節數據解碼爲一組字符,咱們把 ByteBuffer 傳遞給
CharsetDecoder,結果獲得一個 CharBuffer

CharBuffer cb = decoder.decode( inputData );

若是想要處理字符,咱們能夠在程序的此處進行。可是咱們只想無改變地將它寫回,因此沒有什麼要作的。

要寫回數據,咱們必須使用 CharsetEncoder 將它轉換回字節:

ByteBuffer outputData = encoder.encode( cb );

在轉換完成以後,咱們就能夠將數據寫到文件中了。

結束語

結束語

正如您所看到的, NIO 庫有大量的特性。在一些新特性(例如文件鎖定和字符集)提供新功能的同時,許多特性在優化方面也很是優秀。

在基礎層次上,通道和緩衝區能夠作的事情幾乎均可以用原來的面向流的類來完成。可是通道和緩衝區容許以 快得多
的方式完成這些相同的舊操做 ― 事實上接近系統所容許的最大速度。

不過 NIO 最強大的長度之一在於,它提供了一種在 Java 語言中執行進行輸入/輸出的新的(也是迫切須要的)結構化方式。隨諸如緩衝區、通道和異步
I/O 這些概念性(且可實現的)實體而來的,是咱們從新思考 Java 程序中的 I/O過程的機會。這樣,NIO 甚至爲咱們最熟悉的 I/O
過程也帶來了新的活力,同時賦予咱們經過和之前不一樣而且更好的方式執行它們的機會。

參考資料

  • 下載 本教程中的例子的完整源代碼。
  • 關於安裝和配置 JDK 1.4 的更多信息,請參見 SDK 文檔
  • Sun's guide to the new I/O APIs 提供了對 NIO
    的全面介紹,包括一些本教程沒有涵蓋的細節內容。
  • 在線 API 規範
    描述了 NIO 的類和方法,該規範使用的是您瞭解並喜歡的 autodoc 格式。
  • JSR 51 是 Java
    Community Process 文檔,它最早規定了 NIO 的新特性。事實上,JDK 1.4 中實現的 NIO
    是該文檔描述的特性的一個子集。
  • 想得到關於流 I/O(包括問題、解決方案和 NIO 的介紹)的全面介紹嗎?再沒有比 Merlin Hughes 的"Turning streams inside out "
    (developerWorks,2002年7月)更好的了。
  • 固然,還能夠學習教程"Introduction to Java I/O"
    (developerWorks,2000年4月),它討論了 JDK 1.4 以前的 Java I/O 的全部基礎。
  • John Zukowski 在其 Merlin 的魔力 專欄中撰寫了一些關於 NIO 的優秀文章:
  • 經過 Kalagnanam 和 Balu G 的 ""
    Merlin brings nonblocking I/O to the Java platform "

    (developerWorks,2002年3月)進一步瞭解 NIO。
  • Greg Travis 在他的 "JDK 1.4
    Tutorial」
    (Manning 出版社,2002年3月)一書中仔細研究了 NIO。
  • 您能夠在 developerWorks Java 技術專區 找到數百篇關於 Java
    編程的各個方面的文章。


原文地址: https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html
相關文章
相關標籤/搜索