JAVA 拷贝文件的几种方式

JAVA 拷贝文件的几种方式

1. 前言

闲话少叙,今天主要讲讲 JAVA 四种拷贝文件的方式,分析一下他们对内存使用的方式和各自应用的场景,其实也是对之前学过的知识做一个回顾吧,毕竟太久不回顾的话,记忆就像拼图,随着时间流逝就只剩下散落一地的碎片了。

2. 普通拷贝

java 复制代码
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 字节的内存空间,然后不断将文件写入这个内存空间中,并输出到指定文件。

但是需要注意的是,这样的方式虽然简单,但是它的数据流实际上是经过了 4 层传输的

也就是我们的文件需要经过内核到我们 JAVA 虚拟机内部的内存 再到 内核的 socket 缓冲区,再到文件。

3. mmap 内存映射的方式拷贝

java 复制代码
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);
    }
}

第二种由于直接将内存映射在了堆外,也就可以节省普通拷贝中第二步的过程,即不在需要将内核缓冲区中的内容再读到给 java 虚拟机分配的内存中了,比较适合需要 JAVA程序进行文件处理,或者一些小文件的传输

或者也可以通过封装好的

java 复制代码
new RandomAccessFile(file, "r").getChannel().map(FileChannel.MapMode.READ_ONLY, 0, 1024);

来实现,底层是通过 directByteBufferConstructor.newInstance 分配堆外内存来实现的。

4. 零拷贝 sendFile 方式实现

java 复制代码
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 方法,也就是可以跳过用户空间直接实现传输,java 程序中通过 新io 中 file 的 transformTo 方法,底层调用 liunx 内核级的 sendFile 方法,将内核数据直接拷贝到了 socket 缓冲区,从而节省了拷贝次数和消耗。

既然第三种方式相对于第一种和第二种来说,可以完全不经过 java 应用程序,为什么不都直接都用第三种就好了呢?

正是因为它完全不经过 java 程序,也就是说我们无法对文件内容进行二次修改了,第三种方式比较适用于我们将无需经过程序处理的大文件。

需要注意的是,之所以会有程序的内存空间和内核的内存空间的区别,其实主要就是为了隔离,防止恶意程序可以直接访问内核的内存空间

5. 多线程的方式实现拷贝

java 复制代码
// 定义一个线程的数量
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;
    // 定义每个线程开始复制时跳过的字节长度和工作负载大小
    private long skipLen;
    private long workLoad;
    // 定义IO操作的单位大小,这里设置为1024字节
    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);
                 // 创建目标文件的RandomAccessFile,以读写模式打开
                 RandomAccessFile rof = new RandomAccessFile(this.target, "rw");) {
                // 跳过指定偏移量
                bis.skip(this.skipLen);
                // 将读写指针移动到指定偏移量
                rof.seek(this.skipLen);
                byte[] bytes = new byte[IO_UNIT];
                // 计算需要进行的IO操作次数
                long io_num = this.workLoad / IO_UNIT + 1;
                // 如果工作负载大小能被IO_UNIT整除,则IO操作次数减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 文件拷贝来说,本文只是展示和介绍了冰山一角,实际上对于读写流操作,操作系统的实现经过了长时间的演化,从 CPU 中断pagecache,从 sendFileDMA,以及网络传输过程中的 bio nio pollepoll,操作系统经过很多年的演化其中文件和网络的传输处理的复杂程度可想而知。

我想我们可以通过一些小的点管中窥豹,了解一些基础的知识,不用太深入,也能对日常的开发工作和面试有一定帮助。

参考链接 Linux 中的零拷贝

相关推荐
许野平1 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
齐 飞3 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod3 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。4 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man4 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*4 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu4 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s4 小时前
Golang--协程和管道
开发语言·后端·golang
为什么这亚子4 小时前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算
想进大厂的小王5 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构