Java中常見的文件拷貝方式小結
1. 前言
閑話少敘,今天主要講講 JAVA 四種拷貝文件的方式,分析一下他們對內存使用的方式和各自應用的場景,其實也是對之前學過的知識做一個回顧吧,畢竟太久不回顧的話,記憶就像拼圖,隨著時間流逝就只剩下散落一地的碎片了。
2. 普通拷貝
protected void copyFile(File source, File target) { try (FileInputStream is = new FileInputStream(source); FileOutputStream os = new FileOutputStream(target);) { // 分配內存空間 byte[] buffer = new byte[4096]; while (is.read(buffer) != -1) { os.write(buffer); } } catch (Exception e) { logger.error(e); } }
第一種是最簡單的,就是初始化一個輸入輸出流,然后在 JAVA 內部分配一塊 4096 字節(jié)的內存空間,然后不斷將文件寫入這個內存空間中,并輸出到指定文件。
但是需要注意的是,這樣的方式雖然簡單,但是它的數(shù)據(jù)流實際上是經過了 4 層傳輸?shù)?/p>
也就是我們的文件需要經過內核到我們 JAVA 虛擬機內部的內存 再到 內核的 socket 緩沖區(qū),再到文件。
3. mmap 內存映射的方式拷貝
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();) { // 這里開辟的內存直接映射在內核中 ByteBuffer buffer = ByteBuffer.allocateDirect(4096); while (ic.read(buffer) != -1) { buffer.flip(); oc.write(buffer); buffer.clear(); } } catch (Exception e) { logger.error(e); } }
第二種由于直接將內存映射在了堆外,也就可以節(jié)省普通拷貝中第二步的過程,即不在需要將內核緩沖區(qū)中的內容再讀到給 java 虛擬機分配的內存中了,比較適合需要 JAVA程序進行文件處理,或者一些小文件的傳輸
或者也可以通過封裝好的
new RandomAccessFile(file, "r").getChannel().map(FileChannel.MapMode.READ_ONLY, 0, 1024);
來實現(xiàn),底層是通過 directByteBufferConstructor.newInstance
分配堆外內存來實現(xiàn)的。
4. 零拷貝 sendFile 方式實現(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); } }
第三種其實也就是我們俗稱的零拷貝的方式,在 Linux 2.1 版本中,引入了 sendFile
方法,也就是可以跳過用戶空間直接實現(xiàn)傳輸,java 程序中通過 新io 中 file 的 transformTo
方法,底層調用 liunx
內核級的 sendFile
方法,將內核數(shù)據(jù)直接拷貝到了 socket
緩沖區(qū),從而節(jié)省了拷貝次數(shù)和消耗。
既然第三種方式相對于第一種和第二種來說,可以完全不經過 java 應用程序,為什么不都直接都用第三種就好了呢?
正是因為它完全不經過 java 程序,也就是說我們無法對文件內容進行二次修改了,第三種方式比較適用于我們將無需經過程序處理的大文件。
需要注意的是,之所以會有程序的內存空間和內核的內存空間的區(qū)別,其實主要就是為了隔離,防止惡意程序可以直接訪問內核的內存空間
5. 多線程的方式實現(xiàn)拷貝
// 定義一個線程的數(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; // 定義每個線程開始復制時跳過的字節(jié)長度和工作負載大小 private long skipLen; private long workLoad; // 定義IO操作的單位大小,這里設置為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)建目標文件的RandomAccessFile,以讀寫模式打開 RandomAccessFile rof = new RandomAccessFile(this.target, "rw");) { // 跳過指定偏移量 bis.skip(this.skipLen); // 將讀寫指針移動到指定偏移量 rof.seek(this.skipLen); byte[] bytes = new byte[IO_UNIT]; // 計算需要進行的IO操作次數(shù) long io_num = this.workLoad / IO_UNIT + 1; // 如果工作負載大小能被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(); } } }
第四種如果文件特別大的時候,我們還可以通過多線程的方式來進行文件的讀寫,可以充分利用 CPU 多核效率來進一步提升文件的處理效率。
5. 總結
對于 JAVA 文件拷貝來說,本文只是展示和介紹了冰山一角,實際上對于讀寫流操作,操作系統(tǒng)的實現(xiàn)經過了長時間的演化,從 CPU 中斷
到 pagecache
,從 sendFile
到 DMA
,以及網絡傳輸過程中的 bio
nio
poll
和 epoll
,操作系統(tǒng)經過很多年的演化其中文件和網絡的傳輸處理的復雜程度可想而知。
到此這篇關于Java中常見的文件拷貝方式小結的文章就介紹到這了,更多相關Java文件拷貝內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
java使用jdbc連接數(shù)據(jù)庫工具類和jdbc連接mysql數(shù)據(jù)示例
這篇文章主要介紹了java使用jdbc連接數(shù)據(jù)庫的工具類和使用jdbc連接mysql數(shù)據(jù)的示例,需要的朋友可以參考下2014-03-03SpringBoot概述及在idea中創(chuàng)建方式
SpringBoot提供了一種快速使用Spring的方式,基于約定大于配置的思想,可以讓開發(fā)人員不必在配置與邏輯業(yè)務之間進行思維的切換,這篇文章主要介紹了SpringBoot概述及在idea中創(chuàng)建方式,需要的朋友可以參考下2022-09-09Java StampedLock實現(xiàn)原理與最佳實踐記錄
本文介紹了Java 8引入的StampedLock,這是一種多模式同步控制組件,通過“戳”(stamp)標識鎖的狀態(tài),支持寫鎖、悲觀讀鎖和樂觀讀三種模式,StampedLock在特定場景下能夠大幅提升系統(tǒng)性能,特別是在讀多寫少的場景中,感興趣的朋友跟隨小編一起看看吧2025-01-01