JAVA 拷贝文件的几种方式小结

 更新时间:2024年03月26日 09:37:26   作者:码头的薯条  
本文主要介绍了JAVA拷贝文件的几种方式,包含普通拷贝,mmap内存映射的方式拷贝,零拷贝sendFile方式实现和多线程的方式实现拷贝,具有一定的参考价值,感兴趣的可以了解一下

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 层传输的

image.png

也就是我们的文件需要经过内核到我们 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程序进行文件处理,或者一些小文件的传输

image.png

或者也可以通过封装好的

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 缓冲区,从而节省了拷贝次数和消耗。

image.png

既然第三种方式相对于第一种和第二种来说,可以完全不经过 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,操作系统经过很多年的演化其中文件和网络的传输处理的复杂程度可想而知。

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

参考链接 Linux 中的零拷贝

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

相关文章

  • 基于springboot实现一个简单的aop实例

    基于springboot实现一个简单的aop实例

    这篇文章主要介绍了基于springboot实现一个简单的aop,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-11-11
  • Java原子类中的AtomicInteger类详解

    Java原子类中的AtomicInteger类详解

    这篇文章主要介绍了Java原子类中的AtomicInteger类详解,原子类可以保证对"变量"操作的,原子性、有序性、可见性,我们可以通过AtomicInteger类,来看看它们是怎样工作的,需要的朋友可以参考下
    2023-10-10
  • 深入理解java代码实现分治算法

    深入理解java代码实现分治算法

    分治算法是一种递归算法,它将问题划分为几个独立的子问题,然后递归地解决这些子问题,最后将子问题的解合并起来得到原问题的解,本文详细的介绍java分治算法,感兴趣的可以了解一下
    2023-09-09
  • 使用EasyExcel实现简单的Excel表格解析操作

    使用EasyExcel实现简单的Excel表格解析操作

    这篇文章主要介绍了如何使用EasyExcel完成简单的表格解析操作,同时实现了大量数据情况下数据的分次批量入库,并记录每条数据入库的状态,感兴趣的可以了解下
    2025-03-03
  • MyBatis源码剖析之Mapper代理方式详解

    MyBatis源码剖析之Mapper代理方式详解

    这篇文章主要为大家详细介绍了MyBatis中Mapper代理的方式,文中将通过源码为大家进行详细的剖析,感兴趣的小伙伴可以跟随小编一起学习一下
    2022-07-07
  • RocketMQ生产消息与消费消息超详细讲解

    RocketMQ生产消息与消费消息超详细讲解

    这篇文章主要介绍了RocketMQ生产消息与消费消息,RocketMQ可用于以三种方式发送消息:可靠的同步、可靠的异步和单向传输。前两种消息类型是可靠的,因为无论它们是否成功发送都有响应
    2022-12-12
  • Java数据结构及算法实例:快速计算二进制数中1的个数(Fast Bit Counting)

    Java数据结构及算法实例:快速计算二进制数中1的个数(Fast Bit Counting)

    这篇文章主要介绍了Java数据结构及算法实例:快速计算二进制数中1的个数(Fast Bit Counting),本文直接给出实现代码,代码中包含详细注释,需要的朋友可以参考下
    2015-06-06
  • Java 面试题基础知识集锦

    Java 面试题基础知识集锦

    本文主要介绍Java基础面试题集锦,这里整理了面试java工程师的基础知识题锦,有需要的小伙伴可以参考下
    2016-09-09
  • Linux下java环境配置图文方法

    Linux下java环境配置图文方法

    这篇文章主要介绍了Linux下java环境配置图文方法,需要的朋友可以参考下
    2023-06-06
  • 解决kafka消息堆积及分区不均匀的问题

    解决kafka消息堆积及分区不均匀的问题

    这篇文章主要介绍了解决kafka消息堆积及分区不均匀的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09

最新评论