Java NIO 文件通道 FileChannel 用法

FileChannel 提供了一種經過通道來訪問文件的方式,它能夠經過帶參數 position(int) 方法定位到文件的任意位置開始進行操做,還可以將文件映射到直接內存,提升大文件的訪問效率。本文將介紹其詳細用法和原理。css

1. 通道獲取

FileChannel 能夠經過 FileInputStream, FileOutputStream, RandomAccessFile 的對象中的 getChannel() 方法來獲取,也能夠同經過靜態方法 FileChannel.open(Path, OpenOption ...) 來打開。java

1.1 從 FileInputStream / FileOutputStream 中獲取

從 FileInputStream 對象中獲取的通道是以讀的方式打開文件,從 FileOutpuStream 對象中獲取的通道是以寫的方式打開文件。git

FileOutputStream ous = new FileOutputStream(new File("a.txt"));
FileChannel out = ous.getChannel(); // 獲取一個只讀通道
FileInputStream ins = new FileInputStream(new File("a.txt"));
FileChannel in = ins.getChannel();  // 獲取一個只寫通道

1.2 從 RandomAccessFile 中獲取

從 RandomAccessFaile 中獲取的通道取決於 RandomAccessFaile 對象是以什麼方式建立的,"r", "w", "rw" 分別對應着讀模式,寫模式,以及讀寫模式。github

RandomAccessFile file = new RandomAccessFile("a.txt", "rw");
FileChannel channel = file.getChannel(); // 獲取一個可讀寫文件通道

1.3 經過 FileChannel.open() 打開

經過靜態靜態方法 FileChannel.open() 打開的通道能夠指定打開模式,模式經過 StandardOpenOption 枚舉類型指定。spring

FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ); // 以只讀的方式打開一個文件 a.txt 的通道

2. 讀取數據

讀取數據的 read(ByteBuffer buf) 方法返回的值表示讀取到的字節數,若是讀到了文件末尾,返回值爲 -1。讀取數據時,position 會日後移動。緩存

2.1 將數據讀取到單個緩衝區

和通常通道的操做同樣,數據也是須要讀取到1個緩衝區中,而後從緩衝區取出數據。在調用 read 方法讀取數據的時候,能夠傳入參數 position 和 length 來指定開始讀取的位置和長度。app

FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
ByteBuffer buf = ByteBuffer.allocate(5);
while(channel.read(buf)!=-1){
    buf.flip();
    System.out.print(new String(buf.array()));
    buf.clear();
}
channel.close();

2.2 讀取到多個緩衝區

文件通道 FileChannel 實現了 ScatteringByteChannel 接口,能夠將文件通道中的內容同時讀取到多個 ByteBuffer 當中,這在處理包含若干長度固定數據塊的文件時頗有用。dom

ScatteringByteChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
ByteBuffer key = ByteBuffer.allocate(5), value=ByteBuffer.allocate(10);
ByteBuffer[] buffers = new ByteBuffer[]{key, value};
while(channel.read(buffers)!=-1){
    key.flip();
    value.flip();
    System.out.println(new String(key.array()));
    System.out.println(new String(value.array()));
    key.clear();
    value.clear();
}
channel.close();

3. 寫入數據

3.1 從單個緩衝區寫入

單個緩衝區操做也很是簡單,它返回往通道中寫入的字節數。eclipse

FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE);
ByteBuffer buf = ByteBuffer.allocate(5);
byte[] data = "Hello, Java NIO.".getBytes();
for (int i = 0; i < data.length; ) {
    buf.put(data, i, Math.min(data.length - i, buf.limit() - buf.position()));
    buf.flip();
    i += channel.write(buf);
    buf.compact();
}
channel.force(false);
channel.close();

3.2 從多個緩衝區寫入

FileChannel 實現了 GatherringByteChannel 接口,與 ScatteringByteChannel 相呼應。能夠一次性將多個緩衝區的數據寫入到通道中。socket

FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE);
ByteBuffer key = ByteBuffer.allocate(10), value = ByteBuffer.allocate(10);
byte[] data = "017 Robothy".getBytes();
key.put(data, 0, 3);
value.put(data, 4, data.length-4);
ByteBuffer[] buffers = new ByteBuffer[]{key, value};
key.flip();
value.flip();
channel.write(buffers);
channel.force(false); // 將數據刷出到磁盤
channel.close();

3.3 數據刷出

爲了減小訪問磁盤的次數,經過文件通道對文件進行操做以後可能不會當即刷出到磁盤,此時若是系統崩潰,將致使數據的丟失。爲了減小這種風險,在進行了重要數據的操做以後應該調用 force() 方法強制將數據刷出到磁盤。

不管是否對文件進行過修改操做,即便文件通道是以只讀模式打開的,只要調用了 force(metaData) 方法,就會進行一次 I/O 操做。參數 metaData 指定是否將元數據(例如:訪問時間)也刷出到磁盤。

channel.force(false); // 將數據刷出到磁盤,但不包括元數據

4. 文件鎖

能夠經過調用 FileChannel 的 lock() 或者 tryLock() 方法來得到一個文件鎖,獲取鎖的時候能夠指定參數起始位置 position,鎖定大小 size,是否共享 shared。若是沒有指定參數,默認參數爲 position = 0, size = Long.MAX_VALUE, shared = false。

位置 position 和大小 size 不須要嚴格與文件保持一致,position 和 size 都可以超過文件的大小範圍。例如:文件大小爲 100,能夠指定位置爲 200, 大小爲 50;則當文件大小擴展到 250 時,[200,250) 的部分會被鎖住。

shared 參數指定是排他的仍是共享的。要獲取共享鎖,文件通道必須是可讀的;要獲取排他鎖,文件通道必須是可寫的。

因爲 Java 的文件鎖直接映射爲操做系統的文件鎖實現,所以獲取文件鎖時表明的是整個虛擬機,而非當前線程。若操做系統不支持共享的文件鎖,即便指定了文件鎖是共享的,也會被轉化爲排他鎖。

FileLock lock = channel.lock(0, Long.MAX_VALUE, false);// 排它鎖,此時同一操做系統下的其它進程不能訪問 a.txt
System.out.println("Channel locked in exclusive mode.");
Thread.sleep(30 * 1000L); // 鎖住 30 s
lock.release(); // 釋放鎖

lock = channel.lock(0, Long.MAX_VALUE, true); // 共享鎖,此時文件能夠被其它文件訪問
System.out.println("Channel locked in shared mode.");
Thread.sleep(30 * 1000L); // 鎖住 30 s
lock.release();

與 lock() 相比,tryLock() 是非阻塞的,不管是否可以獲取到鎖,它都會當即返回。若 tryLock() 請求鎖定的區域已經被操做系統內的其它的進程鎖住了,則返回 null;而 lock() 會阻塞,直到獲取到了鎖、通道被關閉或者線程被中斷爲止。

5. 通道轉換

普通的讀寫方式是利用一個 ByteBuffer 緩衝區,做爲數據的容器。但若是是兩個通道之間的數據交互,利用緩衝區做爲媒介是多餘的。文件通道容許從一個 ReadableByteChannel 中直接輸入數據,也容許直接往 WritableByteChannel 中寫入數據。實現這兩個操做的分別爲 transferFrom(ReadableByteChannel src, position, count) 和 transferTo(position, count, WritableChannel target) 方法。

這進行通道間的數據傳輸時,這兩個方法比使用 ByteBuffer 做爲媒介的效率要高;不少操做系統支持文件系統緩存,兩個文件之間實際可能並無發生複製。

transferFrom 或者 transferTo 在調用以後並不會改變 position 的位置。

下面示例是一個 spring 源碼中的一個工具方法。

public static void copy(File source, File target) throws IOException {
    FileInputStream sourceOutStream = new FileInputStream(source);
    FileOutputStream targetOutStream = new FileOutputStream(target);
    FileChannel sourceChannel = sourceOutStream.getChannel();
    FileChannel targetChannel = targetOutStream.getChannel();
    sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
    sourceChannel.close();
    targetChannel.close();
    sourceOutStream.close();
    targetOutStream.close();
}

須要注意的是,調用這兩個轉換方法以後,某些狀況下並不保證數據可以所有完成傳輸,確切傳輸了多少字節的數據須要根據返回的值來進行判斷。例如:從一個非阻塞模式下的 SocketChannel 中輸入數據就不可以一次性將數據所有傳輸過來,或者將文件通道的數據傳輸給一個非阻塞模式下的 SocketChannel 不能一次性傳輸過去。

下面給出一個示例,客戶端鏈接到服務端,而後從服務端下載一個叫 video.mp4 文件,文件在當前目錄存在。

錯誤示例:

/** 服務端 **/
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 打開服務通道
serverSocketChannel.bind(new InetSocketAddress(9090)); // 綁定端口號
SocketChannel clientChannel = serverSocketChannel.accept(); // 等待客戶端鏈接,獲取 SocketChannel
FileChannel fileChannel = FileChannel.open(Paths.get("video.mp4"), StandardOpenOption.READ); // 打開文件通道
fileChannel.transferTo(0, fileChannel.size(), clientChannel); // 【可能出錯位置】文件通道數據輸出轉化到 socket 通道,輸出範圍爲整個文件。文件太大將致使輸出不完整

/** 客戶端 **/
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9090)); // 打卡 socket 通道並鏈接到服務端
FileChannel fileChannel = FileChannel.open(Paths.get("video-downloaded.mp4"), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE, StandardOpenOption.CREATE); // 打開文件通道
fileChannel.transferFrom(socketChannel, 0, Long.MAX_VALUE); // 【非阻塞模式下可能出錯】
fileChannel.force(false); // 確保數據刷出到磁盤

正確的姿式是:transferTo/transferFrom 的時候應該用一個循環檢查實際輸出內容大小是否和指望輸出內容大小一致,特別是通道處於非阻塞模式下,極大機率不可以一次傳輸完成。

因此服務端正確的轉換方式是:

long transfered = 0;
while (transfered < fileChannel.size()){
    transfered += fileChannel.transferTo(transfered, fileChannel.size(), clientChannel);
}

本例中客戶端使用的是阻塞模式,服務端通道關閉輸出(socketChannel.shutdownOutput())以後 transferFrom 才退出,服務端正常關閉通道的狀況下數據傳輸不會出錯,這裏就不處理非正常關閉的狀況了。(完整代碼)。

6. 截取文件

FileChannel.truncate(long size) 能夠截取指定的文件,指定大小以後的內容將被丟棄。size 的值能夠超過文件大小,超過的話不會截取任何內容,也不會增長任何內容。

FileChannel fileChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE);
fileChannel.truncate(1);
System.out.println(fileChannel.size()); // 輸出 1
fileChannel.write(ByteBuffer.wrap("Hello".getBytes()));
System.out.println(fileChannel.size()); // 輸出 5
fileChannel.force(true);
fileChannel.close();

7. 映射文件到直接內存

文件通道 FileChannel 能夠將文件的指定範圍映射到程序的地址空間中,映射部分使用字節緩衝區的一個子類 MappedByteBuffer 的對象表示,只要對映射字節緩衝區進行操做就可以達到操做文件的效果。與之相對應的,前面介紹的內容是經過操做文件通道和堆內存中的字節緩衝區 HeapByteBuffer 來達到操做文件的目的。

經過 ByteBuffer.allocate() 分配的緩衝區是一個 HeapByteBuffer,存在於 JVM 堆中;而 FileChannle.map() 將文件映射到直接內存,返回的是一個 MappedByteBuffer,存在於堆外的直接內存中;這塊內存在 MappedByteBuffer 對象自己被回收以前有效。

主存 主存 JVM進程內存 HeapByteBuffer JVM 堆內存 a) HeapByteBuffer 在內存中的位置 b) MappedByteBuffer 在內存中的位置 JVM 堆內存 JVM進程內存 MappedByteBuffer

7.1 內存映射原理

前面使用堆緩衝區 ByteBuffer 和文件通道 FileChannel 對文件的操做使用的是 read()/write() 系統調用。讀取數據時數據從 I/O 設備讀到內核緩存,再從內核緩存複製到用戶空間緩存,這裏是 JVM 的堆內存。而映射磁盤文件是使用 mmap() 系統調用,將文件的指定部分映射到程序地址空間中;數據交互發生在 I/O 設備於用戶空間之間,不須要通過內核空間。

文件 內核空間 用戶空間 緩存 IO設備 緩存 文件 內核空間 用戶空間 緩存 IO設備 緩存 a) 普通 I/O b) 內存映射 I/O

雖然映射磁盤文件減小了一次數據複製,但對於大多數操做系統來講,將文件映射到內存這個操做自己開銷較大;若是操做的文件很小,只有數十KB,映射文件所得到的好處將不及其開銷。所以,只有在操做大文件的時候纔將其映射到直接內存。

7.2 映射緩衝區用法

文件通道 FileChanle 經過成員方法 map(MapMode mode, long position, long size) 將文件映射到應用內存。

FileChannel fileChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE); // 以讀寫的方式打開文件通道
MappedByteBuffer buf = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size()); // 將整個文件映射到內存

mode 表示打開模式,爲枚舉值,其值能夠爲 READ_ONLY, READ_WRITE, PRIVATE。
+ 模式爲 READ_ONLY 時,不能對 buf 進行寫操做;
+ 模式爲 READ_WRITE 時,通道 fileChannel 必須具備讀寫文件的權限;對 buf 進行的寫操做將對文件生效,但不保證當即同步到 I/O 設備;
+ 模式爲 PRIVATE 時,通道 fileChannle 必須對文件有讀寫權限;可是對文件的修改操做不會傳播到 I/O 設備,而是會在內存複製一份數據。此時對文件的修改對其它線程和進程不可見。

position 指定文件的開始映射到內存的位置;

size 指定映射的大小,值爲非負 int 型整數。

調用 map() 方法以後,返回的 MappedByteBuffer 就於 fileChannel 脫離了關係,關閉 fileChannel 對 buf 沒有影響。同時,若是要確保對 buf 修改的數據可以同步到文件 I/O 設備中,須要調用 MappedByteBuffer 中的無參數的 force() 方法,而調用 FileChannel 中的 force(metaData) 方法無效。

此時能夠經過操做緩衝區來操做文件了。不過映射的內容存在於 JVM 程序的堆外內存中,這部份內存是虛擬內存,意味着 buf 中的內容不必定都在物理內存中,要讓這些內容加載到物理內存,能夠調用 MappedByteBuffer 中的 load() 方法。另外,還能夠調用 isLoaded() 來判斷 buf 中的內容是否在物理內存中。

FileChannel fileChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ);
MappedByteBuffer buf = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size());
fileChannel.close();    // 關於文件通道對 buf 沒有影響
System.out.println(buf.capacity()); // 輸出 fileChannel.size()
System.out.println(buf.limit());    // 輸出 fileChannel.size()
System.out.println(buf.position()); // 輸出 0
buf.put((byte)'R'); // 寫入內容
buf.compact();      // 截掉 positoin 以前的內容
buf.force();        // 將數據刷出到 I/O 設備

8. 小結

1)文件通道 FileChannel 可以將數據從 I/O 設備中讀入(read)到字節緩衝區中,或者將字節緩衝區中的數據寫入(write)到 I/O 設備中。

2)文件通道可以轉換到 (transferTo) 一個可寫通道中,也能夠從一個可讀通道轉換而來(transferFrom)。這種方式使用於通道之間地數據傳輸,比使用緩衝區更加高效。

3)文件通道可以將文件的部份內容映射(map)到 JVM 堆外內存中,這種方式適合處理大文件,不適合處理小文件,由於映射過程自己開銷很大。

4)在對文件進行重要的操做以後,應該將數據刷出刷出(force)到磁盤,避免操做系統崩潰致使的數據丟失。

相關文章
相關標籤/搜索