NIO 之 FileChannel

概述

文件通道總是阻塞式的,因此不能被置於非阻塞模式。現代操作系統都有複雜的緩存和預取機制,使得本地磁盤 I/O 操作延遲很少。網絡文件系統一般而言延遲會多些,不過卻也因該優化而受益。 面向流的 I/O 的非阻塞範例對於面向文件的操作並無多大意義,這是由文件 I/O 本質上的不同性質造成的。對於文件 I/O,最強大之處在於異步 I/O( asynchronous I/O),它允許一個進程可以從操作系統請求一個或多個 I/O 操作而不必等待這些操作的完成。發起請求的進程之後會收到它請求的 I/O 操作已完成的通知。

異步 I/O 是在JDK 1.7 才被加入的 java.nio.channels.AsynchronousFileChannel 。

FileChannel

FileChannel對象不能直接創建。一個FileChannel實例只能通過在一個打開的file對象( RandomAccessFile、 FileInputStream或 FileOutputStream)上調用getChannel( )方法
獲取。調用getChannel( )方法會返回一個連接到相同文件的FileChannel對象且該FileChannel對象具有與file對象相同的訪問權限,然後您就可以使用該通道對象來利用強大的FileChannel API了。

FileChannel 線程安全

FileChannel 是線程安全的類,支持多個線程同時併發訪問,但不是所有的方法都能多線程同時併發訪問,比如,文件大小,file postion 等,該方法要想獲取正確的值,只能加鎖訪問。

FileChannel 類結構

public abstract class FileChannel extends AbstractInterruptibleChannel
        implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {
    protected FileChannel() {}

    public static FileChannel open(Path path, OpenOption... options) throws IOException {}

    public abstract int read(ByteBuffer dst) throws IOException;

    public abstract int write(ByteBuffer src) throws IOException;

    public abstract long position() throws IOException;

    public abstract FileChannel position(long newPosition) throws IOException;

    public abstract long size() throws IOException;

    public abstract FileChannel truncate(long size) throws IOException;

    public abstract void force(boolean metaData) throws IOException;

    public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;

    public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;

    public abstract int read(ByteBuffer dst, long position) throws IOException;

    public abstract int write(ByteBuffer src, long position) throws IOException;

    public static class MapMode {
        public static final MapMode READ_ONLY = new MapMode("READ_ONLY");
        public static final MapMode READ_WRITE = new MapMode("READ_WRITE");
        public static final MapMode PRIVATE = new MapMode("PRIVATE");
    }

    public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;

    public abstract FileLock lock(long position, long size, boolean shared) throws IOException;

    public final FileLock lock() throws IOException {}

    public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException;

    public final FileLock tryLock() throws IOException {}

}

open() 方法

public static FileChannel open(Path path, Set<? extends OpenOption> options,
           FileAttribute<?>... attrs) throws IOException {}
public static FileChannel open(Path path, OpenOption... options) throws IOException {}

position() 方法

每個 FileChannel 都有一個叫“file position”的概念。這個 position 值決定文件中哪一處的數據接下來將被讀或者寫。

FileChannel 類爲我們提供了兩種position( )方法。

public abstract long position() throws IOException;
public abstract FileChannel position(long newPosition) throws IOException;
  • 第一種,不帶參數的,返回當前文件的position值。返回值是一個長整型( long),表示文件中的當前字節位置
  • 第二種形式的 position( )方法帶一個 long(長整型) 參數並將通道的 position 設置爲指定值。

position 必須是大於等於0的整數。但是 postion 的大小可以超出文件的大小。

當 position 的位置大於文件的長度時分以下兩種情況:

  1. 調用 read() 方法,無法讀取的數據,相當於讀取到文件末尾。
  2. 調用 write() 方法,會在當前position的位置寫入緩衝區中的字節。寫方法可能會引起產生文件空洞。

文件空洞

當磁盤上一個文件的分配空間小於它的文件大小時會出現“文件空洞”。對於內容稀疏的文件,大多數現代文件系統只爲實際寫入的數據分配磁盤空間(更準確地說,只爲那些寫入數據的文件系統頁分配空間)。假如數據被寫入到文件中非連續的位置上,這將導致文件出現在邏輯上不包含數據的區域( 即“空洞”)。

例如:文件大小爲 10 byte,現在把position 設置爲100,然後調用 write 方法寫入10個字節,現在的文件大小爲 110 字節。而文件系統爲了優化而磁盤中實際只佔用了20字節,其它90個字節未分配空間。當真正寫入的時候才分配磁盤空間。
是否產生文件空洞,取決與文件系統的實現。

truncate() 方法

當需要減少一個文件的 size 時, truncate( )方法會砍掉您所指定的新 size 值之外的所有數據。如果 truncate 的 size 大於文件的 size 該文件不會修改。如果truncate 的 size 小於或等於當前的文件 size 值,該文件會把 truncate 的 size 後面的數據刪掉。如果postion的值大於 truncate size 的值,在 truncate 後會把 postion的值修改爲 truncate size 的值。

    public static void main(String[] args) throws Exception {
        RandomAccessFile fis = new RandomAccessFile(new File("d:\\a.txt"),"rw");
        FileChannel fs =fis.getChannel();
        ByteBuffer bb = ByteBuffer.allocate(10);
        fs.position(100);
        fs.write(bb);
        fs.position(1000);
        System.out.println("原始size:"+fs.size() + "\tposition:"+fs.position());
        fs.truncate(200);
        System.out.println("truncate 200\tsize:"+fs.size()+ "\tposition:"+fs.position());

        fs.truncate(100);
        System.out.println("truncate 100\tsize:"+fs.size()+ "\tposition:"+fs.position());
    }
原始size:110  position:1000
truncate 200    size:110    position:200
truncate 100    size:100    position:100

force() 方法

該方法告訴 FileChannel,強制把所有修改的數據全部寫如到磁盤上。
文件系統可能爲了性能,把要修改的數據先寫入緩存,等緩存寫滿後一塊同步寫入到磁盤中,使用緩存來提高文件的讀寫速度。

transferTo() 和 transferFrom() 方法

transferTo( )和 transferFrom( )方法允許將一個通道交叉連接到另一個通道,而不需要通過一箇中間緩衝區來傳遞數據。只有 FileChannel 類有這兩個方法,因此 channel-to-channel 傳輸中通道之一必須是 FileChannel。您不能在 socket 通道之間直接傳輸數據,不過 socket 通道實現WritableByteChannel 和 ReadableByteChannel 接口,因此文件的內容可以用 transferTo( )方法傳輸給一個 socket 通道,或者也可以用 transferFrom( )方法將數據從一個 socket 通道直接讀取到一個文件中。

直接的通道傳輸不會更新與某個 FileChannel 關聯的 position 值。請求的數據傳輸將從position 參數指定的位置開始,傳輸的字節數不超過 count 參數的值。實際傳輸的字節數會由方法返回,可能少於您請求的字節數。

對於傳輸數據來源是一個文件的 transferTo( )方法,如果 position + count 的值大於文件
的 size 值,傳輸會在文件尾的位置終止。假如傳輸的目的地是一個非阻塞模式的 socket 通道,那麼當發送隊列( send queue) 滿了之後傳輸就可能終止,並且如果輸出隊列( output queue)已滿的話可能不會發送任何數據。類似地,對於 transferFrom( )方法:如果來源 src 是另外一個 FileChannel並且已經到達文件尾,那麼傳輸將提早終止;如果來源 src 是一個非阻塞 socket 通道,只有當前處於隊列中的數據纔會被傳輸(可能沒有數據)。由於網絡數據傳輸的非確定性,阻塞模式的socket 也可能會執行部分傳輸,這取決於操作系統。許多通道實現都是提供它們當前隊列中已有的數據而不是等待您請求的全部數據都準備好。

注意:
NIO,非阻塞通道,不要使用 transferTo 或 tranferFrom 來傳輸數據,傳輸的數據可能會不完整。

map() 方法

map( )的方法,該方法可以在一個打開的文件和一個特殊類型的 ByteBuffer 之間建立一個虛擬內存映射。在 FileChannel 上調用 map( )方法會創建一個由磁盤文件支持的虛擬內存映射( virtual memory mapping)並在那塊虛擬內存空間外部封裝一個 MappedByteBuffer 對象。

由 map( )方法返回的 MappedByteBuffer 對象(直接內存)的行爲在多數方面類似一個基於內存的緩衝區,只不過該對象的數據元素存儲在磁盤上的一個文件中。調用 get( )方法會從磁盤文件中獲取數據。通過文件映射看到的數據同您用常規方法讀取文件看到的內容是完全一樣的。相似地,對映射的緩衝區實現一個 put( )會更新磁盤上的那個文件,並且您做的修改對於該文件的其他閱讀者也是可見的。

通過內存映射機制來訪問一個文件會比使用常規方法讀寫高效得多,甚至比使用通道的效率都高。因爲不需要做明確的系統調用,那會很消耗時間。更重要的是,操作系統的虛擬內存可以自動緩存內存頁( memory page)。這些頁是用系統內存來緩存的,所以不會消耗 Java 虛擬機內存堆( memory heap)。

lock() 方法

調用帶參數的 Lock( )方法會指定文件內部鎖定區域的開始 position 以及鎖定區域的 size。第三個參數 shared 表示您想獲取的鎖是共享的(參數值爲 true)還是獨佔的(參數值爲 false)。要獲得一個共享鎖,您必須先以只讀權限打開文件,而請求獨佔鎖時則需要寫權限。另外,您提供的 position和 size 參數的值不能是負數。

FileChannel 的 lock 支持獲取共享鎖和獨佔鎖(lock 方法的第三個參賽 shared)。是否支持共享鎖還得依賴本地的操作系統實現。並非所有的操作系統和文件系統都支持共享文件鎖。對於那些不支持的,對一個共享鎖的請求會被自動提升爲對獨佔鎖的請求。這可以保證準確性卻可能嚴重影響性能。

鎖的對象是文件而不是通道或線程,如果在同一個進程使用多線程獲取文件鎖,只要一個能獲取到鎖,那麼其它的所遇鹹菜都可以獲取到鎖。

鎖定區域的範圍不一定要限制在文件的 size 值以內,鎖可以擴展從而超出文件尾。因此,我們可以提前把待寫入數據的區域鎖定,我們也可以鎖定一個不包含任何文件內容的區域,比如文件最後一個字節以外的區域。如果之後文件增長到達那塊區域,那麼您的文件鎖就可以保護該區域的文件內容了。相反地,如果您鎖定了文件的某一塊區域,然後文件增長超出了那塊區域,那麼新增加的文件內容將不會受到您的文件鎖的保護。

不帶參數的 lock() 方法,默認獲取的是獨佔鎖,並且鎖定的文件區域是 0 到 Long.MAX_VALUE。

public final FileLock lock() throws IOException {
    return lock(0L, Long.MAX_VALUE, false);
}

文件鎖使用,詳見文章:JAVA 文件鎖 FileLock


想了解更多精彩內容請關注我的公衆號

本人簡書blog地址:http://www.jianshu.com/u/1f0067e24ff8    
點擊這裏快速進入簡書
GIT地址:http://git.oschina.net/brucekankan/
點擊這裏快速進入GIT

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章