Java NIO Channel
通道(Channel)是java.nio的第二個主要創新。它們既不是一個擴充套件也不是一項增強,而是全新、極好的Java I/O示例,提供與I/O服務的直接連線。Channel用於在位元組緩衝區和位於通道另一側的實體(通常是一個檔案或套接字)之間有效地傳輸資料。
多數情況下,通道與作業系統的檔案描述符(File Descriptor)和檔案控制代碼(File Handle)有著一對一的關係。雖然通道比檔案描述符更廣義,但您將經常使用到的多數通道都是連線到開放的檔案描述符的。Channel類提供維持平臺獨立性所需的抽象過程,不過仍然會模擬現代作業系統本身的I/O效能。
通道是一種途徑,藉助該途徑,可以用最小的總開銷來訪問作業系統本身的I/O服務。緩衝區則是通道內部用來發送和接收資料的端點。
類繼承圖:

channel類繼承圖.png
1、channel基礎概念
下面是Channel介面的完整原始碼:
package java.nio.channels; public interface Channel { public boolean isOpen( ); public void close( ) throws IOException; }
與緩衝區不同,通道API主要由介面指定。不同的作業系統上通道實現(Channel Implementation)會有根本性的差異,所以通道API僅僅描述了可以做什麼。因此很自然地,通道實現經常使用作業系統的原生代碼。通道介面允許您以一種受控且可移植的方式來訪問底層的I/O服務。
您可以從頂層的Channel介面看到,對所有通道來說只有兩種共同的操作:檢查一個通道是否開啟(IsOpen())和關閉一個開啟的通道(close())。圖3-2顯示,所有有趣的東西都是那些實現Channel介面以及它的子介面的類。
InterruptibleChannel是一個標記介面,當被通道使用時可以標示該通道是可以中斷的(Interruptible)。如果連線可中斷通道的執行緒被中斷,那麼該通道會以特別的方式工作。大多數但非全部的通道都是可以中斷的。
從Channel介面引申出的其他介面都是面向位元組的子介面,包括Writable ByteChannel 和 ReadableByteChannel。這也正好支援了我們之前所學的:通道只能在位元組緩衝區上操作。層次結構表明其他資料型別的通道也可以從Channel介面引申而來。這是一種很好的類設計,不過非位元組實現是不可能的,因為作業系統都是以位元組的形式實現底層I/O介面的。
java.nio.channels.spi。這兩個類是AbstractInterruptibleChannel和AbstractSelectableChannel,它們分別為可中斷的(interruptible)和可選擇的(selectable)的通道實現提供所需的常用方法。儘管描述通道行為的介面都是在java.nio.channels包中定義的,不過具體的通道實現卻都是從java.nio.channels.spi中的類引申來的。這使得他們可以訪問受保護的方法,而這些方法普通的通道使用者永遠都不會呼叫。
作為通道的一個使用者,您可以放心地忽視SPI包中包含的中間類。這種有點費解的繼承層次只會讓那些使用新通道的使用者感興趣。SPI包允許新通道實現以一種受控且模組化的方式被植入到Java虛擬機器上。這意味著可以使用專為某種作業系統、檔案系統或應用程式而優化的通道來使效能最大化。
1.1、開啟通道
通道是訪問I/O服務的導管。正如我們在第一章中所討論的,I/O可以分為廣義的兩大類別:File I/O和Stream I/O。那麼相應地有兩種型別的通道也就不足為怪了,它們是檔案(file)通道和套接字(socket)通道。您就會發現有一個FileChannel類和三個socket通道類:SocketChannel、ServerSocketChannel和DatagramChannel。
通道可以以多種方式建立。Socket通道有可以直接建立新socket通道的工廠方法。但是一個FileChannel物件卻只能通過在一個開啟的RandomAccessFile、FileInputStream或FileOutputStream物件上呼叫getChannel( )方法來獲取。您不能直接建立一個FileChannel物件。
程式碼示例:
SocketChannel sc = SocketChannel.open( ); sc.connect (new InetSocketAddress ("somehost", someport)); ServerSocketChannel ssc = ServerSocketChannel.open( ); ssc.socket( ).bind (new InetSocketAddress (somelocalport)); DatagramChannel dc = DatagramChannel.open( ); RandomAccessFile raf = new RandomAccessFile ("somefile", "r"); FileChannel fc = raf.getChannel( );
java.net的socket類也有新的getChannel( )方法。這些方法雖然能返回一個相應的socket通道物件,但它們卻並非新通道的來源,RandomAccessFile.getChannel( )方法才是。只有在已經有通道存在的時候,它們才返回與一個socket關聯的通道;它們永遠不會建立新通道。
1.2、使用通道
通道將資料傳輸給ByteBuffer物件或者從ByteBuffer物件獲取資料進行傳輸。
public interface ReadableByteChannel extends Channel { public int read (ByteBuffer dst) throws IOException; } public interface WritableByteChannel extends Channel { public int write (ByteBuffer src) throws IOException; } public interface ByteChannel extends ReadableByteChannel, WritableByteChannel { }
繼承UML圖:

通道繼承UML圖.png
通道可以是單向(unidirectional)或者雙向的(bidirectional)。一個channel類可能實現定義read( )方法的ReadableByteChannel介面,而另一個channel類也許實現WritableByteChannel介面以提供write( )方法。實現這兩種介面其中之一的類都是單向的,只能在一個方向上傳輸資料。如果一個類同時實現這兩個介面,那麼它是雙向的,可以雙向傳輸資料。
ByteChannel介面,該介面引申出了ReadableByteChannel 和WritableByteChannel兩個介面。ByteChannel介面本身並不定義新的API方法,它是一種用來聚集它自己以一個新名稱繼承的多個介面的便捷介面。根據定義,實現ByteChannel介面的通道會同時實現ReadableByteChannel 和WritableByteChannel兩個介面,所以此類通道是雙向的。這是簡化類定義的語法糖(syntactic sugar),它使得用操作器(operator)例項來測試通道物件變得更加簡單。
通道會連線一個特定I/O服務且通道例項(channel instance)的效能受它所連線的I/O服務的特徵限制,記住這很重要。一個連線到只讀檔案的Channel例項不能進行寫操作,即使該例項所屬的類可能有write( )方法。基於此,程式設計師需要知道通道是如何開啟的,避免試圖嘗試一個底層I/O服務不允許的操作。
// A ByteBuffer named buffer contains data to be written FileInputStream input = new FileInputStream (fileName); FileChannel channel = input.getChannel( ); // This will compile but will throw an IOException // because the underlying file is read-only channel.write (buffer);
ByteChannel的read( ) 和write( )方法使用ByteBuffer物件作為引數。兩種方法均返回已傳輸的位元組數,可能比緩衝區的位元組數少甚至可能為零。緩衝區的位置也會發生與已傳輸位元組相同數量的前移。如果只進行了部分傳輸,緩衝區可以被重新提交給通道並從上次中斷的地方繼續傳輸。該過程重複進行直到緩衝區的hasRemaining( )方法返回false值。
通道可以以阻塞(blocking)或非阻塞(nonblocking)模式執行。非阻塞模式的通道永遠不會讓呼叫的執行緒休眠。請求的操作要麼立即完成,要麼返回一個結果表明未進行任何操作。只有面向流的(stream-oriented)的通道,如sockets和pipes才能使用非阻塞模式。
socket通道類從SelectableChannel引申而來。從SelectableChannel引申而來的類可以和支援有條件的選擇(readiness selectio)的選擇器(Selectors)一起使用。將非阻塞I/O和選擇器組合起來可以使您的程式利用多路複用I/O(multiplexed I/O)。
1.3、 關閉通道
與緩衝區不同,通道不能被重複使用。一個開啟的通道即代表與一個特定I/O服務的特定連線並封裝該連線的狀態。當通道關閉時,那個連線會丟失,然後通道將不再連線任何東西。
呼叫通道的close( )方法時,可能會導致在通道關閉底層I/O服務的過程中執行緒暫時阻塞7,哪怕該通道處於非阻塞模式。通道關閉時的阻塞行為(如果有的話)是高度取決於作業系統或者檔案系統的。在一個通道上多次呼叫close( )方法是沒有壞處的,但是如果第一個執行緒在close( )方法中阻塞,那麼在它完成關閉通道之前,任何其他呼叫close( )方法都會阻塞。後續在該已關閉的通道上呼叫close( )不會產生任何操作,只會立即返回。
可以通過isOpen( )方法來測試通道的開放狀態。如果返回true值,那麼該通道可以使用。如果返回false值,那麼該通道已關閉,不能再被使用。嘗試進行任何需要通道處於開放狀態作為前提的操作,如讀、寫等都會導致ClosedChannelException異常。
通道引入了一些與關閉和中斷有關的新行為。如果一個通道實現InterruptibleChannel介面(參見圖3-2),它的行為以下述語義為準:如果一個執行緒在一個通道上被阻塞並且同時被中斷(由呼叫該被阻塞執行緒的interrupt( )方法的另一個執行緒中斷),那麼該通道將被關閉,該被阻塞執行緒也會產生一個ClosedByInterruptException異常。
此外,假如一個執行緒的interrupt status被設定並且該執行緒試圖訪問一個通道,那麼這個通道將立即被關閉,同時將丟擲相同的ClosedByInterruptException異常。執行緒的interrupt status線上程的interrupt( )方法被呼叫時會被設定。我們可以使用isInterrupted( )來測試某個執行緒當前的interrupt status。當前執行緒的interrupt status可以通過呼叫靜態的Thread.interrupted( )方法清除。
“在全部平臺上提供確定的通道行為”這一需求導致了“當I/O操作被中斷時總是關閉通道”這一設計選擇。這個選擇被認為是可接受的,因為大部分時候一個執行緒被中斷就是希望以此來關閉通道。java.nio包中強制使用此行為來避免因作業系統獨特性而導致的困境,因為該困境對I/O區域而言是極其危險的。這也是為增強健壯性(robustness)而採用的一種經典的權衡。
可中斷的通道也是可以非同步關閉的。實現InterruptibleChannel介面的通道可以在任何時候被關閉,即使有另一個被阻塞的執行緒在等待該通道上的一個I/O操作完成。當一個通道被關閉時,休眠在該通道上的所有執行緒都將被喚醒並接收到一個AsynchronousCloseException異常。接著通道就被關閉並將不再可用。
不實現InterruptibleChannel介面的通道一般都是不進行底層原生代碼實現的有特殊用途的通道。這些也許是永遠不會阻塞的特殊用途通道,如舊系統資料流的封裝包或不能實現可中斷語義的writer類等。
2、Scatter/Gather
通道提供了一種被稱為Scatter/Gather的重要新功能(有時也被稱為向量I/O)。Scatter/Gather是一個簡單卻強大的概念,它是指在多個緩衝區上實現一個簡單的I/O操作。對於一個write操作而言,資料是從幾個緩衝區按順序抽取(稱為gather)並沿著通道傳送的。緩衝區本身並不需要具備這種gather的能力(通常它們也沒有此能力)。該gather過程的效果就好比全部緩衝區的內容被連結起來,並在傳送資料前存放到一個大的緩衝區中。對於read操作而言,從通道讀取的資料會按順序被散佈(稱為scatter)到多個緩衝區,將每個緩衝區填滿直至通道中的資料或者緩衝區的最大空間被消耗完。
大多數現代作業系統都支援本地向量I/O(native vectored I/O)。當您在一個通道上請求一個Scatter/Gather操作時,該請求會被翻譯為適當的本地呼叫來直接填充或抽取緩衝區。這是一個很大的進步,因為減少或避免了緩衝區拷貝和系統呼叫。Scatter/Gather應該使用直接的ByteBuffers以從本地I/O獲取最大效能優勢。
public interface ScatteringByteChannel extends ReadableByteChannel { public long read (ByteBuffer [] dsts) throws IOException; public long read (ByteBuffer [] dsts, int offset, int length) throws IOException; } public interface GatheringByteChannel extends WritableByteChannel { public long write(ByteBuffer[] srcs) throws IOException; public long write(ByteBuffer[] srcs, int offset, int length) throws IOException; }

scatter和gather繼承圖.png
從上圖您可以看到,這兩個介面都添加了兩種以緩衝區陣列作為引數的新方法。另外,每種方法都提供了一種帶offset和length引數的形式。
ByteBuffer header = ByteBuffer.allocateDirect (10); ByteBuffer body = ByteBuffer.allocateDirect (80); ByteBuffer [] buffers = { header, body }; int bytesRead = channel.read (buffers);
使用得當的話,Scatter/Gather會是一個極其強大的工具。它允許您委託作業系統來完成辛苦活:將讀取到的資料分開存放到多個儲存桶(bucket)或者將不同的資料區塊合併成一個整體。這是一個巨大的成就,因為作業系統已經被高度優化來完成此類工作了。它節省了您來回移動資料的工作,也就避免了緩衝區拷貝和減少了您需要編寫、除錯的程式碼數量。既然您基本上通過提供資料容器引用來組合資料,那麼按照不同的組合構建多個緩衝區陣列引用,各種資料區塊就可以以不同的方式來組合了。
3、檔案通道
FileChannel類可以實現常用的read,write以及scatter/gather操作,同時它也提供了很多專用於檔案的新方法。這些方法中的許多都是我們所熟悉的檔案操作,不過其他的您可能之前並未接觸過。現在我們將在此對它們全部予以討論。

檔案通道.png
檔案通道總是阻塞式的,因此不能被置於非阻塞模式。現代作業系統都有複雜的快取和預取機制,使得本地磁碟I/O操作延遲很少。網路檔案系統一般而言延遲會多些,不過卻也因該優化而受益。面向流的I/O的非阻塞範例對於面向檔案的操作並無多大意義,這是由檔案I/O本質上的不同性質造成的。對於檔案I/O,最強大之處在於非同步I/O(asynchronous I/O),它允許一個程序可以從作業系統請求一個或多個I/O操作而不必等待這些操作的完成。發起請求的程序之後會收到它請求的I/O操作已完成的通知。
FileChannel物件不能直接建立。一個FileChannel例項只能通過在一個開啟的file物件(RandomAccessFile、FileInputStream或FileOutputStream)上呼叫getChannel( )方法獲取8。呼叫getChannel( )方法會返回一個連線到相同檔案的FileChannel物件且該FileChannel物件具有與file物件相同的訪問許可權,然後您就可以使用該通道物件來利用強大的FileChannel API了:
package java.nio.channels; public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // This is a partial API listing // All methods listed here can throw java.io.IOException public abstract int read (ByteBuffer dst, long position) public abstract int write (ByteBuffer src, long position) public abstract long size( ) public abstract long position( ) public abstract void position (long newPosition) public abstract void truncate (long size) public abstract void force (boolean metaData) public final FileLock lock( ) public abstract FileLock lock (long position, long size, boolean shared) public final FileLock tryLock( ) public abstract FileLock tryLock (long position, long size, boolean shared) public abstract MappedByteBuffer map (MapMode mode, long position, long size) public static class MapMode { public static final MapMode READ_ONLY public static final MapMode READ_WRITE public static final MapMode PRIVATE } public abstract long transferTo (long position, long count, WritableByteChannel target) public abstract long transferFrom (ReadableByteChannel src, long position, long count) }
同大多數通道一樣,只要有可能,FileChannel都會嘗試使用本地I/O服務。FileChannel類本身是抽象的,您從getChannel( )方法獲取的實際物件是一個具體子類(subclass)的一個例項(instance),該子類可能使用原生代碼來實現以上API方法中的一些或全部。
FileChannel物件是執行緒安全(thread-safe)的。多個程序可以在同一個例項上併發呼叫方法而不會引起任何問題,不過並非所有的操作都是多執行緒的(multithreaded)。影響通道位置或者影響檔案大小的操作都是單執行緒的(single-threaded)。如果有一個執行緒已經在執行會影響通道位置或檔案大小的操作,那麼其他嘗試進行此類操作之一的執行緒必須等待。併發行為也會受到底層的作業系統或檔案系統影響。
同大多數I/O相關的類一樣,FileChannel是一個反映Java虛擬機器外部一個具體物件的抽象。FileChannel類保證同一個Java虛擬機器上的所有例項看到的某個檔案的檢視均是一致的,但是Java虛擬機器卻不能對超出它控制範圍的因素提供擔保。通過一個FileChannel例項看到的某個檔案的檢視同通過一個外部的非Java程序看到的該檔案的檢視可能一致,也可能不一致。多個程序發起的併發檔案訪問的語義高度取決於底層的作業系統和(或)檔案系統。一般而言,由執行在不同Java虛擬機器上的FileChannel物件發起的對某個檔案的併發訪問和由非Java程序發起的對該檔案的併發訪問是一致的。
3.1、訪問檔案
每個FileChannel物件都同一個檔案描述符(file descriptor)有一對一的關係,所以上面列出的API方法與在您最喜歡的POSIX(可移植作業系統介面)相容的作業系統上的常用檔案I/O系統呼叫緊密對應也就不足為怪了。名稱也許不盡相同,不過常見的suspect(“可疑分子”)都被集中起來了。您可能也注意到了上面列出的API方法同java.io包中RandomAccessFile類的方法的相似之處了。本質上講,RandomAccessFile類提供的是同樣的抽象內容。在通道出現之前,底層的檔案操作都是通過RandomAccessFile類的方法來實現的。FileChannel模擬同樣的I/O服務,因此它的API自然也是很相似的。
讓我們來進一步看下基本的檔案訪問方法:
public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // This is a partial API listing public abstract long position( ) public abstract void position (long newPosition) public abstract int read (ByteBuffer dst) public abstract int read (ByteBuffer dst, long position) public abstract int write (ByteBuffer src) public abstract int write (ByteBuffer src, long position) public abstract long size( ) public abstract void truncate (long size) public abstract void force (boolean metaData) }
同底層的檔案描述符一樣,每個FileChannel都有一個叫“file position”的概念。這個position值決定檔案中哪一處的資料接下來將被讀或者寫。從這個方面看,FileChannel類同緩衝區很類似,並且MappedByteBuffer類使得我們可以通過ByteBuffer API來訪問檔案資料。
有兩種形式的position( )方法:
第一種,不帶引數的,返回當前檔案的position值。返回值是一個長整型(long),表示檔案中的當前位元組位置;
第二種形式的position( )方法帶一個long(長整型)引數並將通道的position設定為指定值。如果嘗試將通道position設定為一個負值會導致java.lang.IllegalArgumentException異常,不過可以把position設定到超出檔案尾,這樣做會把position設定為指定值而不改變檔案大小。假如在將position設定為超出當前檔案大小時實現了一個read( )方法,那麼會返回一個檔案尾(end-of-file)條件;倘若此時實現的是一個write( )方法則會引起檔案增長以容納寫入的位元組,具體行為類似於實現一個絕對write( )並可能導致出現一個檔案空洞;
FileChannel位置(position)是從底層的檔案描述符獲得的,該position同時被作為通道引用獲取來源的檔案物件共享。這也就意味著一個物件對該position的更新可以被另一個物件看到。
類似於緩衝區的get( ) 和put( )方法,當位元組被read( )或write( )方法傳輸時,檔案position會自動更新。如果position值達到了檔案大小的值(檔案大小的值可以通過size( )方法返回),read( )方法會返回一個檔案尾條件值(-1)。可是,不同於緩衝區的是,如果實現write( )方法時position前進到超過檔案大小的值,該檔案會擴充套件以容納新寫入的位元組。
同樣類似於緩衝區,也有帶position引數的絕對形式的read( )和write( )方法。這種絕對形式的方法在返回值時不會改變當前的檔案position。由於通道的狀態無需更新,因此絕對的讀和寫可能會更加有效率,操作請求可以直接傳到原生代碼。更妙的是,多個執行緒可以併發訪問同一個檔案而不會相互產生干擾。這是因為每次呼叫都是原子性的(atomic),並不依靠呼叫之間系統所記住的狀態。
嘗試在檔案末尾之外的position進行一個絕對讀操作,size( )方法會返回一個end-of-file。在超出檔案大小的position上做一個絕對write( )會導致檔案增加以容納正在被寫入的新位元組。檔案中位於之前end-of-file位置和新新增的位元組起始位置之間區域的位元組的值不是由FileChannel類指定,而是在大多數情況下反映底層檔案系統的語義。取決於作業系統和(或)檔案系統型別,這可能會導致在檔案中出現一個空洞。
當需要減少一個檔案的size時,truncate( )方法會砍掉您所指定的新size值之外的所有資料。如果當前size大於新size,超出新size的所有位元組都會被悄悄地丟棄。如果提供的新size值大於或等於當前的檔案size值,該檔案不會被修改。這兩種情況下,truncate( )都會產生副作用:檔案的position會被設定為所提供的新size值。
force()如果檔案位於一個本地檔案系統,那麼一旦force( )方法返回,即可保證從通道被建立(或上次呼叫force( ))時起的對檔案所做的全部修改已經被寫入到磁碟。對於關鍵操作如事務(transaction)處理來說,這一點是非常重要的,可以保證資料完整性和可靠的恢復。然而,如果檔案位於一個遠端的檔案系統,如NFS上,那麼不能保證待定修改一定能同步到永久儲存器(permanent storage)上,因Java虛擬機器不能做作業系統或檔案系統不能實現的承諾。如果您的程式在面臨系統崩潰時必須維持資料完整性,先去驗證一下您在使用的作業系統和(或)檔案系統在同步修改方面是可以依賴的。該方法告訴通道強制將全部待定的修改都應用到磁碟的檔案上。所有的現代檔案系統都會快取資料和延遲磁碟檔案更新以提高效能。呼叫force( )方法要求檔案的所有待定修改立即同步到磁碟。
3.2、檔案鎖定
絕大多數現代作業系統早就有了檔案鎖定功能,而直到JDK 1.4版本釋出時Java程式設計人員才可以使用檔案鎖(file lock)。在整合許多其他非Java程式時,檔案鎖定顯得尤其重要。此外,它在判優(判斷多個訪問請求的優先級別)一個大系統的多個Java元件發起的訪問時也很有價值。
鎖(lock)可以是共享的(shared)或獨佔的(exclusive)。本節中描述的檔案鎖定特性在很大程度上依賴本地的作業系統實現。並非所有的作業系統和檔案系統都支援共享檔案鎖。對於那些不支援的,對一個共享鎖的請求會被自動提升為對獨佔鎖的請求。這可以保證準確性卻可能嚴重影響效能。
有關FileChannel實現的檔案鎖定模型的一個重要注意項是:鎖的物件是檔案而不是通道或執行緒,這意味著檔案鎖不適用於判優同一臺Java虛擬機器上的多個執行緒發起的訪問。
檔案鎖旨在在程序級別上判優檔案訪問,比如在主要的程式元件之間或者在整合其他供應商的元件時。如果您需要控制多個Java執行緒的併發訪問,您可能需要實施您自己的、輕量級的鎖定方案。那種情形下,記憶體對映檔案(本章後面會進行詳述)可能是一個合適的選擇。
FileLock類封裝一個鎖定的檔案區域。FileLock物件由FileChannel建立並且總是關聯到那個特定的通道例項。您可以通過呼叫channel( )方法來查詢一個lock物件以判斷它是由哪個通道建立的。
一個FileLock物件建立之後即有效,直到它的release( )方法被呼叫或它所關聯的通道被關閉或Java虛擬機器關閉時才會失效。我們可以通過呼叫isValid( )布林方法來測試一個鎖的有效性。一個鎖的有效性可能會隨著時間而改變,不過它的其他屬性——位置(position)、範圍大小(size)和獨佔性(exclusivity)——在建立時即被確定,不會隨著時間而改變。
儘管一個FileLock物件是與某個特定的FileChannel例項關聯的,它所代表的鎖卻是與一個底層檔案關聯的,而不是與通道關聯。因此,如果您在使用完一個鎖後而不釋放它的話,可能會導致衝突或者死鎖。請小心管理檔案鎖以避免出現此問題。一旦您成功地獲取了一個檔案鎖,如果隨後在通道上出現錯誤的話,請務必釋放這個鎖。
4、記憶體對映
新的FileChannel類提供了一個名為map( )的方法,該方法可以在一個開啟的檔案和一個特殊型別的ByteBuffer之間建立一個虛擬記憶體對映。在FileChannel上呼叫map( )方法會建立一個由磁碟檔案支援的虛擬記憶體對映(virtual memory mapping)並在那塊虛擬記憶體空間外部封裝一個MappedByteBuffer物件。
由map( )方法返回的MappedByteBuffer物件的行為在多數方面類似一個基於記憶體的緩衝區,只不過該物件的資料元素儲存在磁碟上的一個檔案中。呼叫get( )方法會從磁碟檔案中獲取資料,此資料反映該檔案的當前內容,即使在對映建立之後檔案已經被一個外部程序做了修改。通過檔案對映看到的資料同您用常規方法讀取檔案看到的內容是完全一樣的。相似地,對對映的緩衝區實現一個put( )會更新磁碟上的那個檔案(假設對該檔案您有寫的許可權),並且您做的修改對於該檔案的其他閱讀者也是可見的。
通過記憶體對映機制來訪問一個檔案會比使用常規方法讀寫高效得多,甚至比使用通道的效率都高。因為不需要做明確的系統呼叫,那會很消耗時間。更重要的是,作業系統的虛擬記憶體可以自動快取記憶體頁(memory page)。這些頁是用系統記憶體來快取的,所以不會消耗Java虛擬機器記憶體堆(memory heap)。
。那些包含索引以及其他需頻繁引用或更新的內容的巨大而結構化檔案能因記憶體對映機制受益非常多。如果同時結合檔案鎖定來保護關鍵區域和控制事務原子性,那您將能瞭解到記憶體對映緩衝區如何可以被很好地利用。
public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // This is a partial API listing public abstract MappedByteBuffer map (MapMode mode, long position,long size) public static class MapMode { public static final MapMode READ_ONLY public static final MapMode READ_WRITE public static final MapMode PRIVATE } }
與檔案鎖的範圍機制不一樣,對映檔案的範圍不應超過檔案的實際大小。如果您請求一個超出檔案大小的對映,檔案會被增大以匹配對映的大小。假如您給size引數傳遞的值是Integer.MAX_VALUE,檔案大小的值會膨脹到超過2.1GB。即使您請求的是一個只讀對映,map( )方法也會嘗試這樣做並且大多數情況下都會丟擲一個IOException異常,因為底層的檔案不能被修改。
FileChannel類定義了代表對映模式的常量,且是使用一個型別安全的列舉而非數字值來定義這些常量。這些常量是FileChannel內部定義的一個內部類(inner class)的靜態欄位,它們可以在編譯時被檢查型別,不過您可以像使用一個數值型常量那樣使用它們。
您應該注意到了沒有unmap( )方法。也就是說,一個對映一旦建立之後將保持有效,直到MappedByteBuffer物件被施以垃圾收集動作為止。同鎖不一樣的是,對映緩衝區沒有繫結到建立它們的通道上。關閉相關聯的FileChannel不會破壞對映,只有丟棄緩衝區物件本身才會破壞該對映。NIO設計師們之所以做這樣的決定是因為當關閉通道時破壞對映會引起安全問題,而解決該安全問題又會導致效能問題。如果您確實需要知道一個對映是什麼時候被破壞的,他們建議使用虛引用(phantom references,參見java.lang.ref.PhantomReference)和一個cleanup執行緒。不過有此需要的概率是微乎其微的。
MemoryMappedBuffer直接反映它所關聯的磁碟檔案。如果對映有效時檔案被在結構上修改,就會產生奇怪的行為(當然具體的行為是取決於作業系統和檔案系統的)。MemoryMappedBuffer有固定的大小,不過它所對映的檔案卻是彈性的。具體來說,如果對映有效時檔案大小變化了,那麼緩衝區的部分或全部內容都可能無法訪問,並將返回未定義的資料或者丟擲未檢查的異常。
所有的MappedByteBuffer物件都是直接的,這意味著它們佔用的記憶體空間位於Java虛擬機器記憶體堆之外。
因為MappedByteBuffers也是ByteBuffers,所以能夠被傳遞SocketChannel之類通道的read( )或write( )以有效傳輸資料給被對映的檔案或從被對映的檔案讀取資料。如能再結合scatter/gather,那麼從記憶體緩衝區和被對映檔案內容中組織資料就變得很容易了。
load( )方法會載入整個檔案以使它常駐記憶體。正如我們在第一章所討論的,一個記憶體對映緩衝區會建立與某個檔案的虛擬記憶體對映。此對映使得作業系統的底層虛擬記憶體子系統可以根據需要將檔案中相應區塊的資料讀進記憶體。已經在記憶體中或通過驗證的頁會佔用實際記憶體空間,並且在它們被讀進RAM時會擠出最近較少使用的其他記憶體頁。
對於大多數程式,特別是互動性的或其他事件驅動(event-driven)的程式而言,為提前載入檔案消耗資源是不划算的。在實際訪問時分攤頁調入開銷才是更好的選擇。讓作業系統根據需要來調入頁意味著不訪問的頁永遠不需要被載入。同預載入整個被對映的檔案相比,這很容易減少I/O活動總次數。
5、Socket通道
新的socket通道類可以執行非阻塞模式並且是可選擇的。這兩個效能可以啟用大程式(如網路伺服器和中介軟體元件)巨大的可伸縮性和靈活性。藉助新的NIO類,一個或幾個執行緒就可以管理成百上千的活動socket連線了並且只有很少甚至可能沒有效能損失。
全部socket通道類(DatagramChannel、SocketChannel和ServerSocketChannel)都是由位於java.nio.channels.spi包中的AbstractSelectableChannel引申而來。這意味著我們可以用一個Selector物件來執行socket通道的有條件的選擇(readiness selection)。

Socket通道.png
DatagramChannel和SocketChannel實現定義讀和寫功能的介面而ServerSocketChannel不實現。ServerSocketChannel負責監聽傳入的連線和建立新的SocketChannel物件,它本身從不傳輸資料。
全部socket通道類(DatagramChannel、SocketChannel和ServerSocketChannel)在被例項化時都會建立一個對等socket物件。這些是我們所熟悉的來自java.net的類(Socket、ServerSocket和DatagramSocket),它們已經被更新以識別通道。對等socket可以通過呼叫socket( )方法從一個通道上獲取。此外,這三個java.net類現在都有getChannel( )方法。
5.1、非阻塞模式
非阻塞I/O是許多複雜的、高效能的程式構建的基礎。
要把一個socket通道置於非阻塞模式,我們要依靠所有socket通道類的公有超級類:SelectableChannel。下面的方法就是關於通道的阻塞模式的:
public abstract class SelectableChannel extends AbstractChannel implements Channel { // This is a partial API listing public abstract void configureBlocking (boolean block) throws IOException; public abstract boolean isBlocking( ); public abstract Object blockingLock( ); }
有條件的選擇(readiness selection)是一種可以用來查詢通道的機制,該查詢可以判斷通道是否準備好執行一個目標操作,如讀或寫。非阻塞I/O和可選擇性是緊密相連的,那也正是管理阻塞模式的API程式碼要在SelectableChannel超級類中定義的原因。
設定或重新設定一個通道的阻塞模式是很簡單的,只要呼叫configureBlocking( )方法即可,傳遞引數值為true則設為阻塞模式,引數值為false值設為非阻塞模式。
5.2、ServerSocketChannel
以下是ServerSocketChannel的完整API:
public abstract class ServerSocketChannel extends AbstractSelectableChannel { public static ServerSocketChannel open( ) throws IOException public abstract ServerSocket socket( ); public abstract ServerSocket accept( ) throws IOException; public final int validOps( ) }
ServerSocketChannel是一個基於通道的socket監聽器。它同我們所熟悉的java.net.ServerSocket執行相同的基本任務,不過它增加了通道語義,因此能夠在非阻塞模式下執行。
用靜態的open( )工廠方法建立一個新的ServerSocketChannel物件,將會返回同一個未繫結的java.net.ServerSocket關聯的通道。該對等ServerSocket可以通過在返回的ServerSocketChannel上呼叫socket( )方法來獲取。作為ServerSocketChannel的對等體被建立的ServerSocket物件依賴通道實現。這些socket關聯的SocketImpl能識別通道。通道不能被封裝在隨意的socket物件外面。
由於ServerSocketChannel沒有bind( )方法,因此有必要取出對等的socket並使用它來繫結到一個埠以開始監聽連線。我們也是使用對等ServerSocket的API來根據需要設定其他的socket選項。
同它的對等體java.net.ServerSocket一樣,ServerSocketChannel也有accept( )方法。一旦您建立了一個ServerSocketChannel並用對等socket綁定了它,然後您就可以在其中一個上呼叫accept( )。如果您選擇在ServerSocket上呼叫accept( )方法,那麼它會同任何其他的ServerSocket表現一樣的行為:總是阻塞並返回一個java.net.Socket物件。如果您選擇在ServerSocketChannel上呼叫accept( )方法則會返回SocketChannel型別的物件,返回的物件能夠在非阻塞模式下執行。假設系統已經有一個安全管理器(security manager),兩種形式的方法呼叫都執行相同的安全檢查。
5.3、SocketChannel
public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel { // This is a partial API listing public static SocketChannel open( ) throws IOException public static SocketChannel open (InetSocketAddress remote) throws IOException public abstract Socket socket( ); public abstract boolean connect (SocketAddress remote) throws IOException; public abstract boolean isConnectionPending( ); public abstract boolean finishConnect( ) throws IOException; public abstract boolean isConnected( ); public final int validOps( ) }
Socket和SocketChannel類封裝點對點、有序的網路連線,類似於我們所熟知並喜愛的TCP/IP網路連線。SocketChannel扮演客戶端發起同一個監聽伺服器的連線。直到連線成功,它才能收到資料並且只會從連線到的地址接收。
每個SocketChannel物件建立時都是同一個對等的java.net.Socket物件串聯的。靜態的open( )方法可以建立一個新的SocketChannel物件,而在新建立的SocketChannel上呼叫socket( )方法能返回它對等的Socket物件;在該Socket上呼叫getChannel( )方法則能返回最初的那個SocketChannel。
我們可以通過在通道上直接呼叫connect( )方法或在通道關聯的Socket物件上呼叫connect( )來將該socket通道連線。一旦一個socket通道被連線,它將保持連線狀態直到被關閉。您可以通過呼叫布林型的isConnected( )方法來測試某個SocketChannel當前是否已連線。
在SocketChannel上並沒有一種connect( )方法可以讓您指定超時(timeout)值,當connect( )方法在非阻塞模式下被呼叫時SocketChannel提供併發連線:它發起對請求地址的連線並且立即返回值。如果返回值是true,說明連線立即建立了(這可能是本地環回連線);如果連線不能立即建立,connect( )方法會返回false且併發地繼續連線建立過程。
面向流的的socket建立連線狀態需要一定的時間,因為兩個待連線系統之間必須進行包對話以建立維護流socket所需的狀態資訊。跨越開放網際網路連線到遠端系統會特別耗時。假如某個SocketChannel上當前正由一個併發連線,isConnectPending( )方法就會返回true值。
Socket通道是執行緒安全的。併發訪問時無需特別措施來保護髮起訪問的多個執行緒,不過任何時候都只有一個讀操作和一個寫操作在進行中。請記住,sockets是面向流的而非包導向的。它們可以保證傳送的位元組會按照順序到達但無法承諾維持位元組分組。某個傳送器可能給一個socket寫入了20個位元組而接收器呼叫read( )方法時卻只收到了其中的3個位元組。剩下的17個位元組還是傳輸中。由於這個原因,讓多個不配合的執行緒共享某個流socket的同一側絕非一個好的設計選擇。
6、管道
java.nio.channels包中含有一個名為Pipe(管道)的類。廣義上講,管道就是一個用來在兩個實體之間單向傳輸資料的導管。Pipe類實現一個管道範例,不過它所建立的管道是程序內(在Java虛擬機器程序內部)而非程序間使用的。

管道.png
Pipe類建立一對提供環回機制的Channel物件。這兩個通道的遠端是連線起來的,以便任何寫在SinkChannel物件上的資料都能出現在SourceChannel物件上。
package java.nio.channels; public abstract class Pipe { public static Pipe open( ) throws IOException public abstract SourceChannel source( ); public abstract SinkChannel sink( ); public static abstract class SourceChannel extends AbstractSelectableChannel implements ReadableByteChannel, ScatteringByteChannel public static abstract class SinkChannel extends AbstractSelectableChannel implements WritableByteChannel, GatheringByteChannel }
Pipe例項是通過呼叫不帶引數的Pipe.open( )工廠方法來建立的。Pipe類定義了兩個巢狀的通道類來實現管路。這兩個類是Pipe.SourceChannel(管道負責讀的一端)和Pipe.SinkChannel(管道負責寫的一端)。這兩個通道例項是在Pipe物件建立的同時被建立的,可以通過在Pipe物件上分別呼叫source( )和sink( )方法來取回。
您不能使用Pipe在作業系統級的程序間建立一個類Unix管道(您可以使用SocketChannel來建立)。Pipe的source通道和sink通道提供類似java.io.PipedInputStream和java.io.PipedOutputStream所提供的功能,不過它們可以執行全部的通道語義。請注意,SinkChannel和SourceChannel都由AbstractSelectableChannel引申而來(所以也是從SelectableChannel引申而來),這意味著pipe通道可以同選擇器一起使用。
Pipes的另一個有用之處是可以用來輔助測試。一個單元測試框架可以將某個待測試的類連線到管道的“寫”端並檢查管道的“讀”端出來的資料。它也可以將被測試的類置於通道的“讀”端並將受控的測試資料寫進其中。
管路所能承載的資料量是依賴實現的(implementation-dependent)。唯一可保證的是寫到SinkChannel中的位元組都能按照同樣的順序在SourceChannel上重現。
7、通道工具類
一個工具類(java.nio.channels.Channels的一個稍微重複的名稱)定義了幾種靜態的工廠方法以使通道可以更加容易地同流和讀寫器互聯。

channels1.png

channels2.png