【计算机IO系类】Java NIO
前面我們講了操作系統的IO模型。而Java NIO并不是只包括IO模型,還包括了對模型的實現和優化。Java NIO是Java 1.4版加入的新特性,雖然Java技術日新月異,但歷經10年,NIO依然為Java技術領域里最為重要的基礎技術棧,而且依據現實的應用趨勢,在可以預見的未來,它仍將繼續在Java技術領域占據重要位置。這篇文章寫的很好,搬運一下!
? ? NIO 主要有三大核心部分:Channel(通道),Buffer(緩沖區), Selector。傳統 IO 基于字節流和字符流進行操作,而 NIO 基于 Channel 和 Buffer(緩沖區)進行操作,數據總是從通道讀取到緩沖區 中,或者從緩沖區寫入到通道中。Selector(選擇區)用于監聽多個通道的事件(比如:連接打開, 數據到達)。因此,單個線程可以監聽多個數據通道。
輸入/輸出:概念性描述
4.1 I/O 簡介
I/O ? 或者輸入/輸出 ? 指的是計算機與外部世界或者一個程序與計算機的其余部分的之間的接口。它對于任何計算機系統都非常關鍵,因而所有 I/O 的主體實際上是內置在操作系統中的。單獨的程序一般是讓系統為它們完成大部分的工作。
在 Java 編程中,直到最近一直使用 流 的方式完成 I/O。所有 I/O 都被視為單個的字節的移動,通過一個稱為 Stream 的對象一次移動一個字節。流 I/O 用于與外部世界接觸。它也在內部使用,用于將對象轉換為字節,然后再轉換回對象。
NIO 與原來的 I/O 有同樣的作用和目的,但是它使用不同的方式? 塊 I/O。正如您將在本教程中學到的,塊 I/O 的效率可以比流 I/O 高許多。
4.2 為什么要使用 NIO?
NIO 的創建目的是為了讓 Java 程序員可以實現高速 I/O 而無需編寫自定義的本機代碼。NIO 將最耗時的 I/O 操作(即填充和提取緩沖區)轉移回操作系統,因而可以極大地提高速度。
4.3 流與塊的比較
原來的 I/O 庫(在?java.io.*中) 與 NIO 最重要的區別是數據打包和傳輸的方式。正如前面提到的,原來的 I/O 以流的方式處理數據,而 NIO 以塊的方式處理數據。
面向流 的 I/O 系統一次一個字節地處理數據。一個輸入流產生一個字節的數據,一個輸出流消費一個字節的數據。為流式數據創建過濾器非常容易。鏈接幾個過濾器,以便每個過濾器只負責單個復雜處理機制的一部分,這樣也是相對簡單的。不利的一面是,面向流的 I/O 通常相當慢。
一個 面向塊 的 I/O 系統以塊的形式處理數據。每一個操作都在一步中產生或者消費一個數據塊。按塊處理數據比按(流式的)字節處理數據要快得多。但是面向塊的 I/O 缺少一些面向流的 I/O 所具有的優雅性和簡單性。
4.4 集成的 I/O
在 JDK 1.4 中原來的 I/O 包和 NIO 已經很好地集成了。?java.io.*?已經以 NIO 為基礎重新實現了,所以現在它可以利用 NIO 的一些特性。例如,?java.io.*?包中的一些類包含以塊的形式讀寫數據的方法,這使得即使在更面向流的系統中,處理速度也會更快。
也可以用 NIO 庫實現標準 I/O 功能。例如,可以容易地使用塊 I/O 一次一個字節地移動數據。但是正如您會看到的,NIO 還提供了原 I/O 包中所沒有的許多好處
5、通道和緩沖區
5.1 概述
?
通道 和 緩沖區 是 NIO 中的核心對象,幾乎在每一個 I/O 操作中都要使用它們。
通道是對原 I/O 包中的流的模擬。到任何目的地(或來自任何地方)的所有數據都必須通過一個 Channel 對象。一個 Buffer 實質上是一個容器對象。發送給一個通道的所有對象都必須首先放到緩沖區中;同樣地,從通道中讀取的任何數據都要讀到緩沖區中。在本節中,您會了解到 NIO 中通道和緩沖區是如何工作的。
5.2 什么是緩沖區?
Buffer?是一個對象, 它包含一些要寫入或者剛讀出的數據。 在 NIO 中加入 Buffer 對象,體現了新庫與原 I/O 的一個重要區別。在面向流的 I/O 中,您將數據直接寫入或者將數據直接讀到 Stream 對象中。
在 NIO 庫中,所有數據都是用緩沖區處理的。在讀取數據時,它是直接讀到緩沖區中的。在寫入數據時,它是寫入到緩沖區中的。任何時候訪問 NIO 中的數據,您都是將它放到緩沖區中。
?緩沖區實質上是一個數組。通常它是一個字節數組,但是也可以使用其他種類的數組。但是一個緩沖區不 僅僅 是一個數組。緩沖區提供了對數據的結構化訪問,而且還可以跟蹤系統的讀/寫進程。
5.3 緩沖區類型
最常用的緩沖區類型是?ByteBuffer。一個 ByteBuffer 可以在其底層字節數組上進行 get/set 操作(即字節的獲取和設置)。
ByteBuffer 不是 NIO 中唯一的緩沖區類型。
Buffer的子類會擁有一塊內存,作為數據的讀寫緩沖區,但是讀寫緩沖區并沒有定義 在Buffer基類,而是定義在具體的子類中。如ByteBuf子類就擁有一個byte[]類型的數組成員 final byte[] hb,作為自己的讀寫緩沖區,數組的元素類型與Buffer子類的操作類型相互對 應。事實上,對于每一種基本 Java 類型都有一種緩沖區類型:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
每一個 Buffer 類都是 Buffer 接口的一個實例。 除了 ByteBuffer,每一個 Buffer 類都有完全一樣的操作,只是它們所處理的數據類型不一樣。因為大多數標準 I/O 操作都使用 ByteBuffer,所以它具有所有共享的緩沖區操作以及一些特有的操作。
現在您可以花一點時間運行 UseFloatBuffer.java(請從本文末的附件下載之),它包含了類型化的緩沖區的一個應用例子。
5.4 什么是通道?
Channel是一個對象,可以通過它讀取和寫入數據。拿 NIO 與原來的 I/O 做個比較,通道就像是流。
正如前面提到的,所有數據都通過 Buffer 對象來處理。您永遠不會將字節直接寫入通道中,相反,您是將數據寫入包含一個或者多個字節的緩沖區。同樣,您不會直接從通道中讀取字節,而是將數據從通道讀入緩沖區,再從緩沖區獲取這個字節。
5.5 通道類型
通道與流的不同之處在于通道是雙向的。而流只是在一個方向上移動(一個流必須是?InputStream?或者?OutputStream?的子類), 而 通道 可以用于讀、寫或者同時用于讀寫。
因為它們是雙向的,所以通道可以比流更好地反映底層操作系統的真實情況。特別是在 UNIX 模型中,底層操作系統通道是雙向的。
Channel和Stream的一個顯著的不同是:Stream是單向的,譬如InputStream是單向的只 讀流,OutputStream是單向的只寫流;而Channel是雙向的,既可以用來進行讀操作,又可 以用來進行寫操作。 NIO中的Channel的主要實現有: 1. FileChannel 用于文件IO操作 2.DatagramChannel 用于UDP的IO操作 3.SocketChannel 用于TCP的傳輸操作 4.ServerSocketChannel 用于TCP連接監聽操作IO事件
什么是IO事件呢?表示通道某種IO操作已經就緒、或者說已經做好了準備。例如,如
果一個新Channel鏈接建立成功了,就會在Server Socket Channel上發生一個IO事件,代表一 個新連接一個準備好,這個IO事件叫做“接收就緒”事件。再例如,一個Channel通道如果 有數據可讀,就會發生一個IO事件,代表該連接數據已經準備好,這個IO事件叫做 “讀就 緒”事件。 Java NIO將NIO事件進行了簡化,只定義了四個事件,這四種事件用SelectionKey的四 個常量來表示: ? SelectionKey.OP_CONNECT ? SelectionKey.OP_ACCEPT ? SelectionKey.OP_READ ? SelectionKey.OP_WRITESelector選擇器
Selector的本質,就是去查詢 這些IO就緒事件,所以,它的名稱就叫做 Selector查詢者。 從編程實現維度來說,IO多路復用編程的第一步,是把通道注冊到選擇器中,第二步 則是通過選擇器所提供的事件查詢(select)方法,這些注冊的通道是否有已經就緒的IO事 件(例如可讀、可寫、網絡連接完成等)。 由于一個選擇器只需要一個線程進行監控,所以,我們可以很簡單地使用一個線程, 通過選擇器去管理多個連接通道。?
6、從理論到實踐:NIO 中的讀和寫
6.1 概述
讀和寫是 I/O 的基本過程。從一個通道中讀取很簡單:只需創建一個緩沖區,然后讓通道將數據讀到這個緩沖區中。寫入也相當簡單:創建一個緩沖區,用數據填充它,然后讓通道用這些數據來執行寫入操作。
在本節中,我們將學習有關在 Java 程序中讀取和寫入數據的一些知識。我們將回顧 NIO 的主要組件(緩沖區、通道和一些相關的方法),看看它們是如何交互以進行讀寫的。在接下來的幾節中,我們將更詳細地分析這其中的每個組件以及其交互。
6.2 從文件中讀取
在我們第一個練習中,我們將從一個文件中讀取一些數據。如果使用原來的 I/O,那么我們只需創建一個?FileInputStream?并從它那里讀取。而在 NIO 中,情況稍有不同:我們首先從 FileInputStream 獲取一個?Channel?對象,然后使用這個通道來讀取數據。
在 NIO 系統中,任何時候執行一個讀操作,您都是從通道中讀取,但是您不是 直接 從通道讀取。因為所有數據最終都駐留在緩沖區中,所以您是從通道讀到緩沖區中。
因此讀取文件涉及三個步驟:(1) 從 FileInputStream 獲取 Channel,(2) 創建 Buffer,(3) 將數據從 Channel 讀到 Buffer 中。
現在,讓我們看一下這個過程。
6.3 三個容易的步驟
第一步是獲取通道。我們從 FileInputStream 獲取通道:
FileInputStream fin = newFileInputStream( "readandshow.txt");
FileChannel fc = fin.getChannel();
下一步是創建緩沖區:
ByteBuffer buffer = ByteBuffer.allocate( 1024);
最后,需要將數據從通道讀到緩沖區中,如下所示:
fc.read( buffer );
您會注意到,我們不需要告訴通道要讀 多少數據 到緩沖區中。每一個緩沖區都有復雜的內部統計機制,它會跟蹤已經讀了多少數據以及還有多少空間可以容納更多的數據。我們將在 緩沖區內部細節 中介紹更多關于緩沖區統計機制的內容。
6.4 寫入文件
在 NIO 中寫入文件類似于從文件中讀取。首先從 FileOutputStream 獲取一個通道:
FileOutputStream fout = newFileOutputStream( "writesomebytes.txt");
FileChannel fc = fout.getChannel();
下一步是創建一個緩沖區并在其中放入一些數據 - 在這里,數據將從一個名為 message 的數組中取出,這個數組包含字符串 "Some bytes" 的 ASCII 字節(本教程后面將會解釋 buffer.flip() 和 buffer.put() 調用)。
ByteBuffer buffer = ByteBuffer.allocate( 1024);
for(intii=0; ii<message.length; ++ii) {
?????buffer.put( message[ii] );
}
buffer.flip();
最后一步是寫入緩沖區中:
fc.write( buffer );
注意在這里同樣不需要告訴通道要寫入多數據。緩沖區的內部統計機制會跟蹤它包含多少數據以及還有多少數據要寫入。
6.5 讀寫結合
下面我們將看一下在結合讀和寫時會有什么情況。我們以一個名為 CopyFile.java 的簡單程序作為這個練習的基礎,它將一個文件的所有內容拷貝到另一個文件中。CopyFile.java 執行三個基本操作:首先創建一個 Buffer,然后從源文件中將數據讀到這個緩沖區中,然后將緩沖區寫入目標文件。這個程序不斷重復 ― 讀、寫、讀、寫 ― 直到源文件結束。
CopyFile 程序讓您看到我們如何檢查操作的狀態,以及如何使用 clear() 和 flip() 方法重設緩沖區,并準備緩沖區以便將新讀取的數據寫到另一個通道中。
6.6 運行 CopyFile 例子
因為緩沖區會跟蹤它自己的數據,所以 CopyFile 程序的內部循環 (inner loop) 非常簡單,如下所示:
fcin.read( buffer );
fcout.write( buffer );
第一行將數據從輸入通道 fcin 中讀入緩沖區,第二行將這些數據寫到輸出通道 fcout 。
6.7 檢查狀態
下一步是檢查拷貝何時完成。當沒有更多的數據時,拷貝就算完成,并且可以在 read() 方法返回 -1 是判斷這一點,如下所示:
intr = fcin.read( buffer );
if(r==-1) {
?????break;
}
6.8 重設緩沖區
最后,在從輸入通道讀入緩沖區之前,我們調用 clear() 方法。同樣,在將緩沖區寫入輸出通道之前,我們調用 flip() 方法,如下所示:
buffer.clear();
intr = fcin.read( buffer );
if(r==-1) {
?????break;
}
buffer.flip();
fcout.write( buffer );
clear() 方法重設緩沖區,使它可以接受讀入的數據。 flip() 方法讓緩沖區可以將新讀入的數據寫入另一個通道。
7、緩沖區內部細節
7.1 概述
本節將介紹 NIO 中兩個重要的緩沖區組件:狀態變量和訪問方法 (accessor)。
狀態變量是前一節中提到的"內部統計機制"的關鍵。每一個讀/寫操作都會改變緩沖區的狀態。通過記錄和跟蹤這些變化,緩沖區就可能夠內部地管理自己的資源。
在從通道讀取數據時,數據被放入到緩沖區。在有些情況下,可以將這個緩沖區直接寫入另一個通道,但是在一般情況下,您還需要查看數據。這是使用 訪問方法 get() 來完成的。同樣,如果要將原始數據放入緩沖區中,就要使用訪問方法 put()。
在本節中,您將學習關于 NIO 中的狀態變量和訪問方法的內容。我們將描述每一個組件,并讓您有機會看到它的實際應用。雖然 NIO 的內部統計機制初看起來可能很復雜,但是您很快就會看到大部分的實際工作都已經替您完成了。您可能習慣于通過手工編碼進行簿記 ― 即使用字節數組和索引變量,現在它已在 NIO 中內部地處理了。
7.2 狀態變量
可以用三個值指定緩沖區在任意時刻的狀態:
position
limit
capacity
這三個變量一起可以跟蹤緩沖區的狀態和它所包含的數據。我們將在下面的小節中詳細分析每一個變量,還要介紹它們如何適應典型的讀/寫(輸入/輸出)進程。在這個例子中,我們假定要將數據從一個輸入通道拷貝到一個輸出通道。
7.3 Position
您可以回想一下,緩沖區實際上就是美化了的數組。在從通道讀取時,您將所讀取的數據放到底層的數組中。 position 變量跟蹤已經寫了多少數據。更準確地說,它指定了下一個字節將放到數組的哪一個元素中。因此,如果您從通道中讀三個字節到緩沖區中,那么緩沖區的 position 將會設置為3,指向數組中第四個元素。
同樣,在寫入通道時,您是從緩沖區中獲取數據。 position 值跟蹤從緩沖區中獲取了多少數據。更準確地說,它指定下一個字節來自數組的哪一個元素。因此如果從緩沖區寫了5個字節到通道中,那么緩沖區的 position 將被設置為5,指向數組的第六個元素。
7.4 Limit
limit 變量表明還有多少數據需要取出(在從緩沖區寫入通道時),或者還有多少空間可以放入數據(在從通道讀入緩沖區時)。
position 總是小于或者等于 limit。
7.5 Capacity
緩沖區的 capacity 表明可以儲存在緩沖區中的最大數據容量。實際上,它指定了底層數組的大小 ― 或者至少是指定了準許我們使用的底層數組的容量。
limit 決不能大于 capacity。
7.6 觀察變量
我們首先觀察一個新創建的緩沖區。出于本例子的需要,我們假設這個緩沖區的 總容量 為8個字節。?
Buffer 的狀態如下所示:
Buffer 的狀態如下所示:
回想一下 ,limit 決不能大于 capacity,此例中這兩個值都被設置為 8。我們通過將它們指向數組的尾部之后(如果有第8個槽,則是第8個槽所在的位置)來說明這點。
position 設置為0。如果我們讀一些數據到緩沖區中,那么下一個讀取的數據就進入 slot 0 。如果我們從緩沖區寫一些數據,從緩沖區讀取的下一個字節就來自 slot 0 。
position 設置如下所示:
由于 capacity 不會改變,所以我們在下面的討論中可以忽略它。
7.7 第一次讀取
現在我們可以開始在新創建的緩沖區上進行讀/寫操作。首先從輸入通道中讀一些數據到緩沖區中。第一次讀取得到三個字節。它們被放到數組中從 position 開始的位置,這時 position 被設置為 0。
讀完之后,position 就增加到 3,如下所示:
limit 沒有改變。
7.8 第二次讀取
在第二次讀取時,我們從輸入通道讀取另外兩個字節到緩沖區中。
這兩個字節儲存在由 position 所指定的位置上, position 因而增加 2:
limit 沒有改變。
7.9 flip
現在我們要將數據寫到輸出通道中。在這之前,我們必須調用 flip() 方法。
這個方法做兩件非常重要的事:
1)它將 limit 設置為當前 position;
2)它將 position 設置為 0。
前一小節中的圖顯示了在 flip 之前緩沖區的情況。
下面是在 flip 之后的緩沖區:
我們現在可以將數據從緩沖區寫入通道了。 position 被設置為 0,這意味著我們得到的下一個字節是第一個字節。 limit 已被設置為原來的 position,這意味著它包括以前讀到的所有字節,并且一個字節也不多。
7.10 第一次寫入
在第一次寫入時,我們從緩沖區中取四個字節并將它們寫入輸出通道。
這使得 position 增加到 4,而 limit 不變,如下所示:
7.11 第二次寫入
我們只剩下一個字節可寫了。 limit在我們調用 flip() 時被設置為 5,并且 position 不能超過 limit。所以最后一次寫入操作從緩沖區取出一個字節并將它寫入輸出通道。
這使得 position 增加到 5,并保持 limit 不變,如下所示:
7.12 clear
最后一步是調用緩沖區的 clear() 方法。這個方法重設緩沖區以便接收更多的字節。?
Clear 做兩種非常重要的事情:
1)它將 limit 設置為與 capacity 相同;
2)它設置 position 為 0。
下圖顯示了在調用 clear() 后緩沖區的狀態:
緩沖區現在可以接收新的數據了。
7.13 訪問方法
到目前為止,我們只是使用緩沖區將數據從一個通道轉移到另一個通道。然而,程序經常需要直接處理數據。例如,您可能需要將用戶數據保存到磁盤。在這種情況下,您必須將這些數據直接放入緩沖區,然后用通道將緩沖區寫入磁盤。
或者,您可能想要從磁盤讀取用戶數據。在這種情況下,您要將數據從通道讀到緩沖區中,然后檢查緩沖區中的數據。
在本節的最后,我們將詳細分析如何使用 ByteBuffer 類的 get() 和 put() 方法直接訪問緩沖區中的數據。
7.14 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() 操作服從 limit 和 position 值 ― 更明確地說,字節是從當前 position 讀取的,而 position 在 get 之后會增加。另一方面,一個 絕對 方法會忽略 limit 和 position 值,也不會影響它們。事實上,它完全繞過了緩沖區的統計方法。
上面列出的方法對應于 ByteBuffer 類。其他類有等價的 get() 方法,這些方法除了不是處理字節外,其它方面是是完全一樣的,它們處理的是與該緩沖區類相適應的類型。
7.15 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() 方法,這些方法除了不是處理字節之外,其它方面是完全一樣的。它們處理的是與該緩沖區類相適應的類型。
7.16 類型化的 get() 和 put() 方法
除了前些小節中描述的 get() 和 put() 方法, ByteBuffer 還有用于讀寫不同類型的值的其他方法。
如下所示:
getByte()
getChar()
getShort()
getInt()
getLong()
getFloat()
getDouble()
putByte()
putChar()
putShort()
putInt()
putLong()
putFloat()
putDouble()
事實上,這其中的每個方法都有兩種類型 ― 一種是相對的,另一種是絕對的。它們對于讀取格式化的二進制數據(如圖像文件的頭部)很有用。
您可以在例子程序 TypesInByteBuffer.java 中看到這些方法的實際應用。
7.17 緩沖區的使用:一個內部循環
下面的內部循環概括了使用緩沖區將數據從輸入通道拷貝到輸出通道的過程。
while(true) {
?????buffer.clear();
?????intr = fcin.read( buffer );
?????if(r==-1) {
???????break;
?????}
?????buffer.flip();
?????fcout.write( buffer );
}
read() 和 write() 調用得到了極大的簡化,因為許多工作細節都由緩沖區完成了。 clear() 和 flip() 方法用于讓緩沖區在讀和寫之間切換。
更多關于java NIO請參考此文:Java NIO使用詳解 - 朱子威 - 博客園
總結
以上是生活随笔為你收集整理的【计算机IO系类】Java NIO的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解决冲突神器(maven helper)
- 下一篇: 大数据概述(二)