溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

FileChannel 怎么在Java 項(xiàng)目中使用

發(fā)布時(shí)間:2021-01-14 14:44:50 來源:億速云 閱讀:145 作者:Leah 欄目:開發(fā)技術(shù)

FileChannel 怎么在Java 項(xiàng)目中使用?相信很多沒有經(jīng)驗(yàn)的人對(duì)此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個(gè)問題。

FileChannel 提供了一種通過通道來訪問文件的方式,它可以通過帶參數(shù) position(int) 方法定位到文件的任意位置開始進(jìn)行操作,還能夠?qū)⑽募成涞街苯觾?nèi)存,提高大文件的訪問效率。本文將介紹其詳細(xì)用法和原理。

1. 通道獲取

FileChannel 可以通過 FileInputStream, FileOutputStream, RandomAccessFile 的對(duì)象中的 getChannel() 方法來獲取,也可以同通過靜態(tài)方法 FileChannel.open(Path, OpenOption ...) 來打開。

1.1 從 FileInputStream / FileOutputStream 中獲取

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

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

1.2 從 RandomAccessFile 中獲取

從 RandomAccessFaile 中獲取的通道取決于 RandomAccessFaile 對(duì)象是以什么方式創(chuàng)建的,"r", "w", "rw" 分別對(duì)應(yīng)著讀模式,寫模式,以及讀寫模式。

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

1.3 通過 FileChannel.open() 打開

通過靜態(tài)靜態(tài)方法 FileChannel.open() 打開的通道可以指定打開模式,模式通過 StandardOpenOption 枚舉類型指定。

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

2. 讀取數(shù)據(jù)

讀取數(shù)據(jù)的 read(ByteBuffer buf) 方法返回的值表示讀取到的字節(jié)數(shù),如果讀到了文件末尾,返回值為 -1。讀取數(shù)據(jù)時(shí),position 會(huì)往后移動(dòng)。

2.1 將數(shù)據(jù)讀取到單個(gè)緩沖區(qū)

和一般通道的操作一樣,數(shù)據(jù)也是需要讀取到1個(gè)緩沖區(qū)中,然后從緩沖區(qū)取出數(shù)據(jù)。在調(diào)用 read 方法讀取數(shù)據(jù)的時(shí)候,可以傳入?yún)?shù) position 和 length 來指定開始讀取的位置和長(zhǎng)度。

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 讀取到多個(gè)緩沖區(qū)

文件通道 FileChannel 實(shí)現(xiàn)了 ScatteringByteChannel 接口,可以將文件通道中的內(nèi)容同時(shí)讀取到多個(gè) ByteBuffer 當(dāng)中,這在處理包含若干長(zhǎng)度固定數(shù)據(jù)塊的文件時(shí)很有用。

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. 寫入數(shù)據(jù)

3.1 從單個(gè)緩沖區(qū)寫入

單個(gè)緩沖區(qū)操作也非常簡(jiǎn)單,它返回往通道中寫入的字節(jié)數(shù)。

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 從多個(gè)緩沖區(qū)寫入

FileChannel 實(shí)現(xiàn)了 GatherringByteChannel 接口,與 ScatteringByteChannel 相呼應(yīng)??梢砸淮涡詫⒍鄠€(gè)緩沖區(qū)的數(shù)據(jù)寫入到通道中。

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); // 將數(shù)據(jù)刷出到磁盤
channel.close();

3.3 數(shù)據(jù)刷出

為了減少訪問磁盤的次數(shù),通過文件通道對(duì)文件進(jìn)行操作之后可能不會(huì)立即刷出到磁盤,此時(shí)如果系統(tǒng)崩潰,將導(dǎo)致數(shù)據(jù)的丟失。為了減少這種風(fēng)險(xiǎn),在進(jìn)行了重要數(shù)據(jù)的操作之后應(yīng)該調(diào)用 force() 方法強(qiáng)制將數(shù)據(jù)刷出到磁盤。

無論是否對(duì)文件進(jìn)行過修改操作,即使文件通道是以只讀模式打開的,只要調(diào)用了 force(metaData) 方法,就會(huì)進(jìn)行一次 I/O 操作。參數(shù) metaData 指定是否將元數(shù)據(jù)(例如:訪問時(shí)間)也刷出到磁盤。

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

4. 文件鎖

可以通過調(diào)用 FileChannel 的 lock() 或者 tryLock() 方法來獲得一個(gè)文件鎖,獲取鎖的時(shí)候可以指定參數(shù)起始位置 position,鎖定大小 size,是否共享 shared。如果沒有指定參數(shù),默認(rèn)參數(shù)為 position = 0, size = Long.MAX_VALUE, shared = false。

位置 position 和大小 size 不需要嚴(yán)格與文件保持一致,position 和 size 均可以超過文件的大小范圍。例如:文件大小為 100,可以指定位置為 200, 大小為 50;則當(dāng)文件大小擴(kuò)展到 250 時(shí),[200,250) 的部分會(huì)被鎖住。

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

由于 Java 的文件鎖直接映射為操作系統(tǒng)的文件鎖實(shí)現(xiàn),因此獲取文件鎖時(shí)代表的是整個(gè)虛擬機(jī),而非當(dāng)前線程。若操作系統(tǒng)不支持共享的文件鎖,即使指定了文件鎖是共享的,也會(huì)被轉(zhuǎn)化為排他鎖。

FileLock lock = channel.lock(0, Long.MAX_VALUE, false);// 排它鎖,此時(shí)同一操作系統(tǒng)下的其它進(jìn)程不能訪問 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); // 共享鎖,此時(shí)文件可以被其它文件訪問
System.out.println("Channel locked in shared mode.");
Thread.sleep(30 * 1000L); // 鎖住 30 s
lock.release();

與 lock() 相比,tryLock() 是非阻塞的,無論是否能夠獲取到鎖,它都會(huì)立即返回。若 tryLock() 請(qǐng)求鎖定的區(qū)域已經(jīng)被操作系統(tǒng)內(nèi)的其它的進(jìn)程鎖住了,則返回 null;而 lock() 會(huì)阻塞,直到獲取到了鎖、通道被關(guān)閉或者線程被中斷為止。

5. 通道轉(zhuǎn)換

普通的讀寫方式是利用一個(gè) ByteBuffer 緩沖區(qū),作為數(shù)據(jù)的容器。但如果是兩個(gè)通道之間的數(shù)據(jù)交互,利用緩沖區(qū)作為媒介是多余的。文件通道允許從一個(gè) ReadableByteChannel 中直接輸入數(shù)據(jù),也允許直接往 WritableByteChannel 中寫入數(shù)據(jù)。實(shí)現(xiàn)這兩個(gè)操作的分別為 transferFrom(ReadableByteChannel src, position, count) 和 transferTo(position, count, WritableChannel target) 方法。

這進(jìn)行通道間的數(shù)據(jù)傳輸時(shí),這兩個(gè)方法比使用 ByteBuffer 作為媒介的效率要高;很多操作系統(tǒng)支持文件系統(tǒng)緩存,兩個(gè)文件之間實(shí)際可能并沒有發(fā)生復(fù)制。

transferFrom 或者 transferTo 在調(diào)用之后并不會(huì)改變 position 的位置。

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

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();
}

需要注意的是,調(diào)用這兩個(gè)轉(zhuǎn)換方法之后,某些情況下并不保證數(shù)據(jù)能夠全部完成傳輸,確切傳輸了多少字節(jié)的數(shù)據(jù)需要根據(jù)返回的值來進(jìn)行判斷。例如:從一個(gè)非阻塞模式下的 SocketChannel 中輸入數(shù)據(jù)就不能夠一次性將數(shù)據(jù)全部傳輸過來,或者將文件通道的數(shù)據(jù)傳輸給一個(gè)非阻塞模式下的 SocketChannel 不能一次性傳輸過去。

下面給出一個(gè)示例,客戶端連接到服務(wù)端,然后從服務(wù)端下載一個(gè)叫 video.mp4 文件,文件在當(dāng)前目錄存在。

錯(cuò)誤示例:

/** 服務(wù)端 **/
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 打開服務(wù)通道
serverSocketChannel.bind(new InetSocketAddress(9090)); // 綁定端口號(hào)
SocketChannel clientChannel = serverSocketChannel.accept(); // 等待客戶端連接,獲取 SocketChannel
FileChannel fileChannel = FileChannel.open(Paths.get("video.mp4"), StandardOpenOption.READ); // 打開文件通道
fileChannel.transferTo(0, fileChannel.size(), clientChannel); // 【可能出錯(cuò)位置】文件通道數(shù)據(jù)輸出轉(zhuǎn)化到 socket 通道,輸出范圍為整個(gè)文件。文件太大將導(dǎo)致輸出不完整

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

正確的姿勢(shì)是:transferTo/transferFrom 的時(shí)候應(yīng)該用一個(gè)循環(huán)檢查實(shí)際輸出內(nèi)容大小是否和期望輸出內(nèi)容大小一致,特別是通道處于非阻塞模式下,極大概率不能夠一次傳輸完成。

所以服務(wù)端正確的轉(zhuǎn)換方式是:

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

本例中客戶端使用的是阻塞模式,服務(wù)端通道關(guān)閉輸出(socketChannel.shutdownOutput())之后 transferFrom 才退出,服務(wù)端正常關(guān)閉通道的情況下數(shù)據(jù)傳輸不會(huì)出錯(cuò),這里就不處理非正常關(guān)閉的情況了。(完整代碼)。

6. 截取文件

FileChannel.truncate(long size) 可以截取指定的文件,指定大小之后的內(nèi)容將被丟棄。size 的值可以超過文件大小,超過的話不會(huì)截取任何內(nèi)容,也不會(huì)增加任何內(nèi)容。

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. 映射文件到直接內(nèi)存

文件通道 FileChannel 可以將文件的指定范圍映射到程序的地址空間中,映射部分使用字節(jié)緩沖區(qū)的一個(gè)子類 MappedByteBuffer 的對(duì)象表示,只要對(duì)映射字節(jié)緩沖區(qū)進(jìn)行操作就能夠達(dá)到操作文件的效果。與之相對(duì)應(yīng)的,前面介紹的內(nèi)容是通過操作文件通道和堆內(nèi)存中的字節(jié)緩沖區(qū) HeapByteBuffer 來達(dá)到操作文件的目的。

通過 ByteBuffer.allocate() 分配的緩沖區(qū)是一個(gè) HeapByteBuffer,存在于 JVM 堆中;而 FileChannle.map() 將文件映射到直接內(nèi)存,返回的是一個(gè) MappedByteBuffer,存在于堆外的直接內(nèi)存中;這塊內(nèi)存在 MappedByteBuffer 對(duì)象本身被回收之前有效。

7.1 內(nèi)存映射原理

前面使用堆緩沖區(qū) ByteBuffer 和文件通道 FileChannel 對(duì)文件的操作使用的是 read()/write() 系統(tǒng)調(diào)用。讀取數(shù)據(jù)時(shí)數(shù)據(jù)從 I/O 設(shè)備讀到內(nèi)核緩存,再從內(nèi)核緩存復(fù)制到用戶空間緩存,這里是 JVM 的堆內(nèi)存。而映射磁盤文件是使用 mmap() 系統(tǒng)調(diào)用,將文件的指定部分映射到程序地址空間中;數(shù)據(jù)交互發(fā)生在 I/O 設(shè)備于用戶空間之間,不需要經(jīng)過內(nèi)核空間。

雖然映射磁盤文件減少了一次數(shù)據(jù)復(fù)制,但對(duì)于大多數(shù)操作系統(tǒng)來說,將文件映射到內(nèi)存這個(gè)操作本身開銷較大;如果操作的文件很小,只有數(shù)十KB,映射文件所獲得的好處將不及其開銷。因此,只有在操作大文件的時(shí)候才將其映射到直接內(nèi)存。

7.2 映射緩沖區(qū)用法

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

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

mode 表示打開模式,為枚舉值,其值可以為 READ_ONLY, READ_WRITE, PRIVATE。
+ 模式為 READ_ONLY 時(shí),不能對(duì) buf 進(jìn)行寫操作;
+ 模式為 READ_WRITE 時(shí),通道 fileChannel 必須具有讀寫文件的權(quán)限;對(duì) buf 進(jìn)行的寫操作將對(duì)文件生效,但不保證立即同步到 I/O 設(shè)備;
+ 模式為 PRIVATE 時(shí),通道 fileChannle 必須對(duì)文件有讀寫權(quán)限;但是對(duì)文件的修改操作不會(huì)傳播到 I/O 設(shè)備,而是會(huì)在內(nèi)存復(fù)制一份數(shù)據(jù)。此時(shí)對(duì)文件的修改對(duì)其它線程和進(jìn)程不可見。

position 指定文件的開始映射到內(nèi)存的位置;

size 指定映射的大小,值為非負(fù) int 型整數(shù)。

調(diào)用 map() 方法之后,返回的 MappedByteBuffer 就于 fileChannel 脫離了關(guān)系,關(guān)閉 fileChannel 對(duì) buf 沒有影響。同時(shí),如果要確保對(duì) buf 修改的數(shù)據(jù)能夠同步到文件 I/O 設(shè)備中,需要調(diào)用 MappedByteBuffer 中的無參數(shù)的 force() 方法,而調(diào)用 FileChannel 中的 force(metaData) 方法無效。

此時(shí)可以通過操作緩沖區(qū)來操作文件了。不過映射的內(nèi)容存在于 JVM 程序的堆外內(nèi)存中,這部分內(nèi)存是虛擬內(nèi)存,意味著 buf 中的內(nèi)容不一定都在物理內(nèi)存中,要讓這些內(nèi)容加載到物理內(nèi)存,可以調(diào)用 MappedByteBuffer 中的 load() 方法。另外,還可以調(diào)用 isLoaded() 來判斷 buf 中的內(nèi)容是否在物理內(nèi)存中。

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(); // 關(guān)于文件通道對(duì) 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'); // 寫入內(nèi)容
buf.compact();  // 截掉 positoin 之前的內(nèi)容
buf.force();  // 將數(shù)據(jù)刷出到 I/O 設(shè)備

8. 小結(jié)

1)文件通道 FileChannel 能夠?qū)?shù)據(jù)從 I/O 設(shè)備中讀入(read)到字節(jié)緩沖區(qū)中,或者將字節(jié)緩沖區(qū)中的數(shù)據(jù)寫入(write)到 I/O 設(shè)備中。

2)文件通道能夠轉(zhuǎn)換到 (transferTo) 一個(gè)可寫通道中,也可以從一個(gè)可讀通道轉(zhuǎn)換而來(transferFrom)。這種方式使用于通道之間地?cái)?shù)據(jù)傳輸,比使用緩沖區(qū)更加高效。

3)文件通道能夠?qū)⑽募牟糠謨?nèi)容映射(map)到 JVM 堆外內(nèi)存中,這種方式適合處理大文件,不適合處理小文件,因?yàn)橛成溥^程本身開銷很大。

4)在對(duì)文件進(jìn)行重要的操作之后,應(yīng)該將數(shù)據(jù)刷出刷出(force)到磁盤,避免操作系統(tǒng)崩潰導(dǎo)致的數(shù)據(jù)丟失。

看完上述內(nèi)容,你們掌握 FileChannel 怎么在Java 項(xiàng)目中使用的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI