您好,登錄后才能下訂單哦!
FileChannel 怎么在Java 項目中使用?相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
FileChannel 提供了一種通過通道來訪問文件的方式,它可以通過帶參數 position(int) 方法定位到文件的任意位置開始進行操作,還能夠將文件映射到直接內存,提高大文件的訪問效率。本文將介紹其詳細用法和原理。
FileChannel 可以通過 FileInputStream, FileOutputStream, RandomAccessFile 的對象中的 getChannel() 方法來獲取,也可以同通過靜態方法 FileChannel.open(Path, OpenOption ...) 來打開。
1.1 從 FileInputStream / FileOutputStream 中獲取
從 FileInputStream 對象中獲取的通道是以讀的方式打開文件,從 FileOutpuStream 對象中獲取的通道是以寫的方式打開文件。
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" 分別對應著讀模式,寫模式,以及讀寫模式。
RandomAccessFile file = new RandomAccessFile("a.txt", "rw"); FileChannel channel = file.getChannel(); // 獲取一個可讀寫文件通道
1.3 通過 FileChannel.open() 打開
通過靜態靜態方法 FileChannel.open() 打開的通道可以指定打開模式,模式通過 StandardOpenOption 枚舉類型指定。
FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ); // 以只讀的方式打開一個文件 a.txt 的通道
讀取數據的 read(ByteBuffer buf) 方法返回的值表示讀取到的字節數,如果讀到了文件末尾,返回值為 -1。讀取數據時,position 會往后移動。
2.1 將數據讀取到單個緩沖區
和一般通道的操作一樣,數據也是需要讀取到1個緩沖區中,然后從緩沖區取出數據。在調用 read 方法讀取數據的時候,可以傳入參數 position 和 length 來指定開始讀取的位置和長度。
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 當中,這在處理包含若干長度固定數據塊的文件時很有用。
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.1 從單個緩沖區寫入
單個緩沖區操作也非常簡單,它返回往通道中寫入的字節數。
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 相呼應。可以一次性將多個緩沖區的數據寫入到通道中。
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); // 將數據刷出到磁盤,但不包括元數據
可以通過調用 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() 會阻塞,直到獲取到了鎖、通道被關閉或者線程被中斷為止。
普通的讀寫方式是利用一個 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 才退出,服務端正常關閉通道的情況下數據傳輸不會出錯,這里就不處理非正常關閉的情況了。(完整代碼)。
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();
文件通道 FileChannel 可以將文件的指定范圍映射到程序的地址空間中,映射部分使用字節緩沖區的一個子類 MappedByteBuffer 的對象表示,只要對映射字節緩沖區進行操作就能夠達到操作文件的效果。與之相對應的,前面介紹的內容是通過操作文件通道和堆內存中的字節緩沖區 HeapByteBuffer 來達到操作文件的目的。
通過 ByteBuffer.allocate() 分配的緩沖區是一個 HeapByteBuffer,存在于 JVM 堆中;而 FileChannle.map() 將文件映射到直接內存,返回的是一個 MappedByteBuffer,存在于堆外的直接內存中;這塊內存在 MappedByteBuffer 對象本身被回收之前有效。
7.1 內存映射原理
前面使用堆緩沖區 ByteBuffer 和文件通道 FileChannel 對文件的操作使用的是 read()/write() 系統調用。讀取數據時數據從 I/O 設備讀到內核緩存,再從內核緩存復制到用戶空間緩存,這里是 JVM 的堆內存。而映射磁盤文件是使用 mmap() 系統調用,將文件的指定部分映射到程序地址空間中;數據交互發生在 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 設備
1)文件通道 FileChannel 能夠將數據從 I/O 設備中讀入(read)到字節緩沖區中,或者將字節緩沖區中的數據寫入(write)到 I/O 設備中。
2)文件通道能夠轉換到 (transferTo) 一個可寫通道中,也可以從一個可讀通道轉換而來(transferFrom)。這種方式使用于通道之間地數據傳輸,比使用緩沖區更加高效。
3)文件通道能夠將文件的部分內容映射(map)到 JVM 堆外內存中,這種方式適合處理大文件,不適合處理小文件,因為映射過程本身開銷很大。
4)在對文件進行重要的操作之后,應該將數據刷出刷出(force)到磁盤,避免操作系統崩潰導致的數據丟失。
看完上述內容,你們掌握 FileChannel 怎么在Java 項目中使用的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。