Java中常见的文件拷贝方式小结

 更新时间:2024年03月26日 09:21:47   作者:码头的薯条  
这篇文章主要为大家详细介绍了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 字节的内存空间,然后不断将文件写入这个内存空间中,并输出到指定文件。

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

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

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

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

或者也可以通过封装好的

new RandomAccessFile(file, "r").getChannel().map(FileChannel.MapMode.READ_ONLY, 0, 1024);

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

4. 零拷贝 sendFile 方式实现

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. 多线程的方式实现拷贝

// 定义一个线程的数量
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,操作系统经过很多年的演化其中文件和网络的传输处理的复杂程度可想而知。

到此这篇关于Java中常见的文件拷贝方式小结的文章就介绍到这了,更多相关Java文件拷贝内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringBoot+jpa配置如何根据实体类自动创建表

    SpringBoot+jpa配置如何根据实体类自动创建表

    这篇文章主要介绍了SpringBoot+jpa配置如何根据实体类自动创建表,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • Java JDK 动态代理的使用方法示例

    Java JDK 动态代理的使用方法示例

    Java 动态代理机制以巧妙的方式近乎完美地实践了代理模式的设计理念。下面这篇文章主要给大家分享了关于Java JDK 动态代理的使用方法示例,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-07-07
  • Java简单实现session保存到redis的方法示例

    Java简单实现session保存到redis的方法示例

    这篇文章主要介绍了Java简单实现session保存到redis的方法,结合实例形式分析了Java将session存入redis缓存服务器的相关设置、实现技巧与操作注意事项,需要的朋友可以参考下
    2018-05-05
  • Java CAS原子操作详解

    Java CAS原子操作详解

    在synchronized的优化过程中我们看到大量使用了CAS操作,CAS全称Compare And Set(或Compare And Swap),简单来说CAS操作就是一个虚拟机实现的原子操作
    2023-02-02
  • Vue3源码解读effectScope API及实现原理

    Vue3源码解读effectScope API及实现原理

    这篇文章主要为大家介绍了Vue3源码解读effectScope API及实现原理,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • Java多线程中的Phaser详解

    Java多线程中的Phaser详解

    这篇文章主要介绍了Java多线程中的Phaser详解,Pahser是一个可以重复使用的同步屏障,Phaser是按照不同阶段执行线程的,它本身维护着一个叫 phase 的成员变量代表当前执行的阶段,需要的朋友可以参考下
    2023-11-11
  • jstack报错Unable to open socket file解决

    jstack报错Unable to open socket file解决

    这篇文章主要为大家介绍了jstack报错Unable to open socket file的解决方法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-02-02
  • 详解Java使用Pipeline对Redis批量读写(hmset&hgetall)

    详解Java使用Pipeline对Redis批量读写(hmset&hgetall)

    本篇文章主要介绍了Java使用Pipeline对Redis批量读写(hmset&hgetall),具有一定的参考价值,有兴趣的可以了解一下。
    2016-12-12
  • 详谈Map的key、value值的数据类型不能为基本类型的原因

    详谈Map的key、value值的数据类型不能为基本类型的原因

    这篇文章主要介绍了详谈Map的key、value值的数据类型不能为基本类型的原因,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09
  • Java中的OneToMany的使用方法

    Java中的OneToMany的使用方法

    这篇文章主要介绍了Java中的OneToMany的使用方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-04-04

最新评论