Java中常見的文件拷貝方式小結(jié)
1. 前言
閑話少敘,今天主要講講 JAVA 四種拷貝文件的方式,分析一下他們對內(nèi)存使用的方式和各自應(yīng)用的場景,其實(shí)也是對之前學(xué)過的知識(shí)做一個(gè)回顧吧,畢竟太久不回顧的話,記憶就像拼圖,隨著時(shí)間流逝就只剩下散落一地的碎片了。
2. 普通拷貝
protected void copyFile(File source, File target) {
try (FileInputStream is = new FileInputStream(source);
FileOutputStream os = new FileOutputStream(target);) {
// 分配內(nèi)存空間
byte[] buffer = new byte[4096];
while (is.read(buffer) != -1) {
os.write(buffer);
}
} catch (Exception e) {
logger.error(e);
}
}
第一種是最簡單的,就是初始化一個(gè)輸入輸出流,然后在 JAVA 內(nèi)部分配一塊 4096 字節(jié)的內(nèi)存空間,然后不斷將文件寫入這個(gè)內(nèi)存空間中,并輸出到指定文件。
但是需要注意的是,這樣的方式雖然簡單,但是它的數(shù)據(jù)流實(shí)際上是經(jīng)過了 4 層傳輸?shù)?/p>

也就是我們的文件需要經(jīng)過內(nèi)核到我們 JAVA 虛擬機(jī)內(nèi)部的內(nèi)存 再到 內(nèi)核的 socket 緩沖區(qū),再到文件。
3. mmap 內(nèi)存映射的方式拷貝
protected void copyFile(File source, File target) {
try (FileInputStream is = new FileInputStream(source);
FileOutputStream os = new FileOutputStream(target);
FileChannel ic = is.getChannel();
FileChannel oc = os.getChannel();) {
// 這里開辟的內(nèi)存直接映射在內(nèi)核中
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
while (ic.read(buffer) != -1) {
buffer.flip();
oc.write(buffer);
buffer.clear();
}
} catch (Exception e) {
logger.error(e);
}
}
第二種由于直接將內(nèi)存映射在了堆外,也就可以節(jié)省普通拷貝中第二步的過程,即不在需要將內(nèi)核緩沖區(qū)中的內(nèi)容再讀到給 java 虛擬機(jī)分配的內(nèi)存中了,比較適合需要 JAVA程序進(jìn)行文件處理,或者一些小文件的傳輸

或者也可以通過封裝好的
new RandomAccessFile(file, "r").getChannel().map(FileChannel.MapMode.READ_ONLY, 0, 1024);
來實(shí)現(xiàn),底層是通過 directByteBufferConstructor.newInstance 分配堆外內(nèi)存來實(shí)現(xiàn)的。
4. 零拷貝 sendFile 方式實(shí)現(xiàn)
protected void copyFile(File source, File target) {
try {
if (!target.exists()) {
target.createNewFile();
}
} catch (Exception e) {
logger.error(e);
}
try (FileChannel is = new RandomAccessFile(source, "r").getChannel();
FileChannel os = new RandomAccessFile(target, "rw").getChannel()) {
is.transferTo(0, source.length(), os);
} catch (Exception e) {
logger.error(e);
}
}
第三種其實(shí)也就是我們俗稱的零拷貝的方式,在 Linux 2.1 版本中,引入了 sendFile 方法,也就是可以跳過用戶空間直接實(shí)現(xiàn)傳輸,java 程序中通過 新io 中 file 的 transformTo 方法,底層調(diào)用 liunx 內(nèi)核級的 sendFile 方法,將內(nèi)核數(shù)據(jù)直接拷貝到了 socket 緩沖區(qū),從而節(jié)省了拷貝次數(shù)和消耗。

既然第三種方式相對于第一種和第二種來說,可以完全不經(jīng)過 java 應(yīng)用程序,為什么不都直接都用第三種就好了呢?
正是因?yàn)樗耆唤?jīng)過 java 程序,也就是說我們無法對文件內(nèi)容進(jìn)行二次修改了,第三種方式比較適用于我們將無需經(jīng)過程序處理的大文件。
需要注意的是,之所以會(huì)有程序的內(nèi)存空間和內(nèi)核的內(nèi)存空間的區(qū)別,其實(shí)主要就是為了隔離,防止惡意程序可以直接訪問內(nèi)核的內(nèi)存空間
5. 多線程的方式實(shí)現(xiàn)拷貝
// 定義一個(gè)線程的數(shù)量
private Integer threadCount = 5;
protected void copyFile(File source, File target) {
long workLoad = source.length() / threadCount;
for (Integer i = 0; i < threadCount; i++) {
ThreadFileRunnable threadFileRunnable = new ThreadFileRunnable(source, target, i * workLoad, workLoad);
new Thread(threadFileRunnable, "copy-thread" + i).start();
}
}
private class ThreadFileRunnable implements Runnable {
private File source;
private File target;
// 定義每個(gè)線程開始復(fù)制時(shí)跳過的字節(jié)長度和工作負(fù)載大小
private long skipLen;
private long workLoad;
// 定義IO操作的單位大小,這里設(shè)置為1024字節(jié)
private final int IO_UNIT = 1024;
public ThreadFileRunnable (File source, File target, long skipLen, long workLoad) {
this.source = source;
this.target = target;
this.skipLen = skipLen;
this.workLoad = workLoad;
}
@Override
public void run() {
try {
try (FileInputStream is = new FileInputStream(this.source);
BufferedInputStream bis = new BufferedInputStream(is);
// 創(chuàng)建目標(biāo)文件的RandomAccessFile,以讀寫模式打開
RandomAccessFile rof = new RandomAccessFile(this.target, "rw");) {
// 跳過指定偏移量
bis.skip(this.skipLen);
// 將讀寫指針移動(dòng)到指定偏移量
rof.seek(this.skipLen);
byte[] bytes = new byte[IO_UNIT];
// 計(jì)算需要進(jìn)行的IO操作次數(shù)
long io_num = this.workLoad / IO_UNIT + 1;
// 如果工作負(fù)載大小能被IO_UNIT整除,則IO操作次數(shù)減1
if (this.workLoad % IO_UNIT == 0) {
io_num--;
}
int count = bis.read(bytes);
while (io_num != 0) {
rof.write(bytes,0,count);
count = bis.read(bytes,0,count);
io_num--;
}
}
} catch (Exception e) {
// 捕獲并打印異常信息
e.printStackTrace();
}
}
}
第四種如果文件特別大的時(shí)候,我們還可以通過多線程的方式來進(jìn)行文件的讀寫,可以充分利用 CPU 多核效率來進(jìn)一步提升文件的處理效率。
5. 總結(jié)
對于 JAVA 文件拷貝來說,本文只是展示和介紹了冰山一角,實(shí)際上對于讀寫流操作,操作系統(tǒng)的實(shí)現(xiàn)經(jīng)過了長時(shí)間的演化,從 CPU 中斷 到 pagecache,從 sendFile 到 DMA,以及網(wǎng)絡(luò)傳輸過程中的 bio nio poll 和 epoll,操作系統(tǒng)經(jīng)過很多年的演化其中文件和網(wǎng)絡(luò)的傳輸處理的復(fù)雜程度可想而知。
到此這篇關(guān)于Java中常見的文件拷貝方式小結(jié)的文章就介紹到這了,更多相關(guān)Java文件拷貝內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Lombok的詳細(xì)使用及優(yōu)缺點(diǎn)總結(jié)
最近在學(xué)Mybatis,接觸到了Lombok的使用,所以寫一篇文章記錄一下,包括lombok的安裝及使用優(yōu)缺點(diǎn),感興趣的朋友跟隨小編一起看看吧2021-07-07
java使用jdbc連接數(shù)據(jù)庫工具類和jdbc連接mysql數(shù)據(jù)示例
這篇文章主要介紹了java使用jdbc連接數(shù)據(jù)庫的工具類和使用jdbc連接mysql數(shù)據(jù)的示例,需要的朋友可以參考下2014-03-03
SpringBoot概述及在idea中創(chuàng)建方式
SpringBoot提供了一種快速使用Spring的方式,基于約定大于配置的思想,可以讓開發(fā)人員不必在配置與邏輯業(yè)務(wù)之間進(jìn)行思維的切換,這篇文章主要介紹了SpringBoot概述及在idea中創(chuàng)建方式,需要的朋友可以參考下2022-09-09
運(yùn)行jar程序時(shí)添加vm參數(shù)的方法
下面小編就為大家?guī)硪黄\(yùn)行jar程序時(shí)添加vm參數(shù)的方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-02-02
Java StampedLock實(shí)現(xiàn)原理與最佳實(shí)踐記錄
本文介紹了Java 8引入的StampedLock,這是一種多模式同步控制組件,通過“戳”(stamp)標(biāo)識(shí)鎖的狀態(tài),支持寫鎖、悲觀讀鎖和樂觀讀三種模式,StampedLock在特定場景下能夠大幅提升系統(tǒng)性能,特別是在讀多寫少的場景中,感興趣的朋友跟隨小編一起看看吧2025-01-01
詳解Java Web項(xiàng)目啟動(dòng)執(zhí)行順序
這篇文章主要介紹了詳解Java Web項(xiàng)目啟動(dòng)執(zhí)行順序,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-06-06
如何利用@PreAuthorize注解自定義權(quán)限校驗(yàn)
通過使用@PreAuthorize注解實(shí)現(xiàn)開放接口的權(quán)限校驗(yàn),具體步驟包括開啟全局方法安全、編寫自定義鑒權(quán)方法、創(chuàng)建自定義異常類、在統(tǒng)一異常處理類中捕獲異常并處理,最后在需要鑒權(quán)的接口上貼上注解2024-12-12

