Channel
通道(Channel)的作用有類似于流(Stream),用于傳輸文件或者網絡上的數據。
上圖中,箭頭就相當于通道。一個不是很準確的例子:把通道想象成鐵軌,緩沖區則是列車,鐵軌的起始與終點則可以是socket,文件系統和我們的程序。假如當我們在代碼中要寫入數據到一份文件的時候,我們先把列車(緩沖區)裝滿,然后把列車(緩沖區)放置到鐵軌上(通道),數據就被傳遞到通道的另一端,文件系統。讀取文件則相反,文件的內容被裝到列車上,傳遞到程序這一側,然后我們在代碼中就可以讀取這個列車中的內容(讀取緩沖區)。
通道與傳統的流還是有一些區別的:
通道可以同時支持讀寫(不是一定支持),而流只支持單方向的操作,比如輸入流只能讀,輸出流只能寫。
通道可以支持異步的讀或寫,而流是同步的。
通道的讀取或寫入是通過緩沖區來進行的,而流則寫入或返回字節。
FileChannel
通道大致上可以分為兩類:文件通道和socket通道。看一下文件通道:
文件通道可以由以下幾個方法獲得:
RandomAccessFile file = new RandomAccessFile(new File(fileName), "rw");
FileChannel channel = file.getChannel();
FileInputStream stream = new FileInputStream(new File(fileName));
FileChannel channel = stream.getChannel();
FileOutputStream stream = new FileOutputStream(new File(fileName));
FileChannel channel = stream.getChannel();
FileChannel channel = FileChannel.open(Paths.get(fileName));
FileChannel 類結構:
可見FileChannel實現了讀寫接口、聚集、發散接口,以及文件鎖功能。下面會提到。
看一下FileChannel的基本方法:
public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {
// 這里僅列出部分API
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)
}
在通道出現之前,底層的文件操作都是通過RandomAccessFile類的方法來實現的。FileChannel模擬同樣的 I/O 服務,因此它們的API自然也是很相似的。
上圖是FileChannel、RandomAccessFile 和 POSIX I/O system calls 三者在方法上的對應關系。
POSIX接口我們在上一篇文章中也略有提及,他是一個系統級別的接口。下面看一下這幾個接口,主要也是和上一篇文章文件描述符的介紹做一個呼應。
- position()和position(long newPosition)
position()返回當前文件的position值,position(long newPosition)將當前position設置為指定值。當字節被read()或write()方法傳輸時,文件position會自動更新。
position的含義與Buffer類中的position含義相似,都是指向下一個字節讀取的位置。
回想一下介紹文件描述符的文章當中提到,當進程打開一個文件時,內核就會創建一個新的file對象,這個file對象有一個字段loff_t f_pos描述了文件的當前位置,position相當于loff_t f_pos的映射。由此可知,如果是使用同一文件描述符讀取文件,那么他們的position是相互影響的:
RandomAccessFile file = new RandomAccessFile(new File(fileName), "rw");
FileChannel channel = file.getChannel();
System.out.println("position: " + channel.position());
file.seek(30);
System.out.println("position: " + channel.position());
打印如下:
position: 0
position: 30
這是因為,file與channel使用了同一個文件描述符。如果新建另一個相同文件的通道,那么他們之間的position不會相互影響,因為使用了不同的文件描述符,指向不同的file對象。
- truncate(long size)
當需要減少一個文件的size時,truncate()方法會砍掉指定的size值之外的所有數據。這個方法要求通道具有寫權限。
如果當前size大于給定size,超出給定size的所有字節都會被刪除。如果提供的新size值大于或等于當前的文件size值,該文件不會被修改。
RandomAccessFile file = new RandomAccessFile(new File(fileName), "rw");
FileChannel channel = file.getChannel();
System.out.println("size: " + channel.size());
System.out.println("position: " + channel.position());
System.out.println("trucate: 90");
channel.truncate(90);
System.out.println("size: " + channel.size());
System.out.println("position: " + channel.position());
打印如下:
size: 100
position: 0
trucate: 90
size: 90
position: 0
- force(boolean metaData)
force()方法告訴通道強制將全部待定的修改都應用到磁盤的文件上。
如果文件位于一個本地文件系統,那么一旦force()方法返回,即可保證從通道被創建(或上次調用force())時起的對文件所做的全部修改已經被寫入到磁盤。但是,如果文件位于一個遠程的文件系統,如NFS上,那么不能保證待定修改一定能同步到永久存儲器。
force()方法的布爾型參數表示在方法返回值前文件的元數據(metadata)是否也要被同步更新到磁盤。元數據指文件所有者、訪問權限、最后一次修改時間等信息。
FileChannel對象是線程安全的。如果有一個線程已經在執行會影響通道位置或文件大小的操作,那么其他嘗試進行此類操作之一的線程必須等待。
ReadableByteChannel、WritableByteChannel
通道可以是單向或者雙向的。
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{
}
實現ReadableByteChannel或WritableByteChannel其中之一的channel是單向的,只可以讀或者寫。如果一個類同時實現了這兩種接口,那么他就具備了雙向傳輸的能力。
java為我們提供了一個接口ByteChannel,同時繼承了上述兩個接口。所以,實現了ByteChannel接口的類可以讀,也可以寫。
在FlieChannel這一節中我們知道,文件在不同的方式下以不同的權限打開。比如FileInputStream.getChannel()
方法返回一個FileChannel實例,FileChannel是個抽象類,間接的實現了ByteChannel接口,也就意味著提供了read和write接口。但是FileInputStream.getChannel()
方法返回的FileChannel實際上是只讀的,很簡單,因為FileInputStream本身就是個輸入流啊~在這樣一個通道上調用write方法將拋出NonWritableChannelException異常,因為FileInputStream對象總是以read-only的權限打開通道。看一下代碼:
FileInputStream.getChannel()
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
// 第三個參數指定通道是否可讀,第四個參數指定通道是否可寫
channel = FileChannelImpl.open(fd, path, true, false, this);
/*
* Increment fd's use count. Invoking the channel's close()
* method will result in decrementing the use count set for
* the channel.
*/
fd.incrementAndGetUseCount();
}
return channel;
}
}
同樣的,FileOutputStream.getChannel()
返回的通道是不可讀的。
InterruptibleChannel
InterruptibleChannel是一個標記接口,當被通道使用時可以標示該通道是可以中斷的。
如果一個線程在一個通道上處于阻塞狀態時被中斷(另外一個線程調用該線程的interrupt()方法設置中斷狀態),那么該通道將被關閉,該被阻塞線程也會產生一個ClosedByInterruptException異常。也就是說,假如一個線程的interrupt status被設置并且該線程試圖訪問一個通道,那么這個通道將立即被關閉,同時將拋出相同的ClosedByInterruptException異常。
在java任務取消中提到了,傳統的java io 在讀寫時阻塞,是不會響應中斷的。解決辦法就是使用InterruptibleChannel,在線程被中斷時可以關閉通道并返回。
可中斷的通道也是可以異步關閉。實現InterruptibleChannel接口的通道可以在任何時候被關閉,即使有另一個被阻塞的線程在等待該通道上的一個I/O操作完成。當一個通道被關閉時,休眠在該通道上的所有線程都將被喚醒并接收到一個AsynchronousCloseException異常。接著通道就被關閉并將不再可用。
Scatter/Gather
發散(Scatter)讀取是將數據讀入多個緩沖區(緩沖區數組)的操作。通道將數據依次填滿到每個緩沖區當中。
匯聚(Gather)寫出是將多個緩沖區(緩沖區數組)數據依次寫入到通道的操作。
在FileChannel中提到的兩個接口,提供了發散匯聚的功能:
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;
}
發散匯聚在某些場景下是很有用的,比如有一個消息協議格式分為head和body(比如http協議),我們在接收這樣一個消息的時候,通常的做法是把數據一下子都讀過來,然后解析他。使用通道的發散功能會使這個過程變得簡單:
// head數據128字節
ByteBuffer header = ByteBuffer.allocate(128);
// body數據1024字節
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
通道會依次填滿這個buffer數組的每個buffer,如果一個buffer滿了,就移動到下一個buffer。很自然的把head和body的數據分開了,但是要注意head和body的數據長度必須是固定的,因為channel只有填滿一個buffer之后才會移動到下一個buffer。
FileLock
摘抄一段oracle官網上FileLock的介紹吧,感覺說的挺清楚了。(因為懶,就不翻譯了,讀起來不是很費勁)
A token representing a lock on a region of a file.
A file-lock object is created each time a lock is acquired on a file via one of the lock or tryLock methods of the FileChannel class, or the lock or tryLock methods of the AsynchronousFileChannel class.A file-lock object is initially valid. It remains valid until the lock is released by invoking the release method, by closing the channel that was used to acquire it, or by the termination of the Java virtual machine, whichever comes first. The validity of a lock may be tested by invoking its >isValid method.
A file lock is either exclusive or shared. A shared lock prevents other concurrently-running programs from acquiring an overlapping exclusive lock, but does allow them to acquire overlapping shared locks. An exclusive lock prevents other programs from acquiring an overlapping lock of either type. >Once it is released, a lock has no further effect on the locks that may be acquired by other programs.
Whether a lock is exclusive or shared may be determined by invoking its isShared method. Some platforms do not support shared locks, in which case a request for a shared lock is automatically converted into a request for an exclusive lock.
The locks held on a particular file by a single Java virtual machine do not overlap. The overlaps method may be used to test whether a candidate lock range overlaps an existing lock.
A file-lock object records the file channel upon whose file the lock is held, the type and validity of the lock, and the position and size of the locked region. Only the validity of a lock is subject to change over time; all other aspects of a lock's state are immutable.
File locks are held on behalf of the entire Java virtual machine. They are not suitable for controlling access to a file by multiple threads within the same virtual machine.
File-lock objects are safe for use by multiple concurrent threads.
Platform dependencies
This file-locking API is intended to map directly to the native locking facility of the underlying operating system. Thus the locks held on a file should be visible to all programs that have access to the file, regardless of the language in which those programs are written.
Whether or not a lock actually prevents another program from accessing the content of the locked region is system-dependent and therefore unspecified. The native file-locking facilities of some systems are merely advisory, meaning that programs must cooperatively observe a known locking protocol in >order to guarantee data integrity. On other systems native file locks are mandatory, meaning that if one program locks a region of a file then other programs are actually prevented from accessing that region in a way that would violate the lock. On yet other systems, whether native file locks are >advisory or mandatory is configurable on a per-file basis. To ensure consistent and correct behavior across platforms, it is strongly recommended that the locks provided by this API be used as if they were advisory locks.
On some systems, acquiring a mandatory lock on a region of a file prevents that region from being mapped into memory, and vice versa. Programs that combine locking and mapping should be prepared for this combination to fail.
On some systems, closing a channel releases all locks held by the Java virtual machine on the underlying file regardless of whether the locks were acquired via that channel or via another channel open on the same file. It is strongly recommended that, within a program, a unique channel be used to >acquire all locks on any given file.
Some network filesystems permit file locking to be used with memory-mapped files only when the locked regions are page-aligned and a whole multiple of the underlying hardware's page size. Some network filesystems do not implement file locks on regions that extend past a certain position, often 230 >or 231. In general, great care should be taken when locking files that reside on network filesystems.
FileLock可以由以下幾個方法獲得:
public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {
// 這里僅列出部分API
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)
}
其中,lock是阻塞的,tryLock是非阻塞的。position和size決定了鎖定的區域,shared決定了文件鎖是共享的還是獨占的。
不帶參數的lock方法等價于fileChannel.lock(0L, Long.MAX_VALUE, false)
,tryLock亦然。
lock方法是響應中斷的,當線程被中斷時方法拋出FileLockInterruptionException異常。如果通道被另外一個線程關閉,該暫停線程將恢復并產生一個 AsynchronousCloseException異常。
上面還提到了,文件鎖是針對于進程級別的。如果有多個進程同時對一個文件鎖定,并且其中有獨占鎖的話,這些鎖的申請會被串行化。
如果是同一個進程(Jvm實例)的多個線程同時請求同一個文件區域的lock的話,會拋出OverlappingFileLockException異常。
Channel-to-Channel
FileChannel提供了接口,用于通道和通道之間的直接傳輸。
public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {
// 這里僅列出部分API
public abstract long transferTo (long position, long count, WritableByteChannel target)
public abstract long transferFrom (ReadableByteChannel src, long position, long count)
}
只有FileChannel類有這兩個方法,因此Channel-to-Channel傳輸中通道之一必須是FileChannel。不能在socket通道之間直接傳輸數據,不過socket通道實現WritableByteChannel和ReadableByteChannel接口,因此文件的內容可以用transferTo()方法傳輸給一個socket通道,或者也可以用transferFrom()方法將數據從一個socket通道直接讀取到一個文件中。
直接的通道傳輸不會更新與某個FileChannel關聯的position值。請求的數據傳輸將從position參數指定的位置開始,傳輸的字節數不超過count參數的值。實際傳輸的字節數會由方法返回。
直接通道傳輸的另一端如果是socket通道并且處于非阻塞模式的話,數據的傳輸將具有不確定性。比如,transferFrom從socket通道讀取數據,如果socket中的數據尚未準備好,那么方法將直接返回。
例子:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel, position, count);
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);