RandomAccessFile学习笔记

文章目录

  • RandomAccessFile学习笔记
    • 前言
    • 1、RandomAccessFile基本介绍
      • 1.1 RandomAccessFile相关基本概念
      • 1.2 RandomAccessFile家族体系
    • 2、RandomAccessFile基本使用
      • 2.1 RandomAccessFile常用API介绍
      • 2.2 RandomAccessFile常用API演示
      • 2.3 RandomAccessFile实现断点续传
    • 1、RandomAccessFile基本介绍
      • 1.1 RandomAccessFile相关基本概念
      • 1.2 RandomAccessFile家族体系
    • 2、RandomAccessFile基本使用
      • 2.1 RandomAccessFile常用API介绍
      • 2.2 RandomAccessFile常用API演示
      • 2.3 RandomAccessFile实现断点续传

RandomAccessFile学习笔记

前言

本文将简要介绍RandomAccessFile这个类的使用,主要包括RandomAccessFile相关的一些概念,常见API的使用,如何利用RandomAccessFile实现一个断点续传的效果

1、RandomAccessFile基本介绍

1.1 RandomAccessFile相关基本概念

  • RandomAccessFile是什么

    RandomAccessFile 是 Java 中用于对文件进行随机访问的类。与普通的输入输出流不同,RandomAccessFile 允许在文件中任意位置读写数据。

  • RandomAccessFile的作用有哪些

    • 随机访问:与其他输入输出流不同,RandomAccessFile 允许在文件中任意位置进行读写操作,可以自由地定位文件指针。
    • 读写基本数据类型RandomAccessFile 实现了 DataInputDataOutput 接口,提供了方便的方法用于读取和写入基本数据类型。
    • 支持读写字节数组:除了读写基本数据类型外,还可以通过 read(byte[] buffer)write(byte[] buffer) 方法来读写字节数组。
    • 支持文件截断:可以使用 setLength(long newLength) 方法来调整文件的长度,将文件截断或扩展为指定的大小。

    常见的应用场景是使用 RandomAccessFile 实现断点续传

    注意事项RandomAccessFile只能读写文件(也就是字节数组)类型的数据,并不能读写流类型的数据

  • RandomAccessFile的四种访问模式

    访问模式特点
    r(read,只读模式)能读文件,不能写文件、持久化
    rw(read write,读写模式)能读、写文件,不能持久化
    rws(read write sync,同步读写模式)能读、写文件,每次写操作都会持久化
    rwd(read write data,同步写模式)能读、写文件,只有在调用close()、getFD().sync()、关闭程序时才进行持久化
    • r”(只读模式):使用只读模式打开文件,只能对文件进行读取操作,无法修改文件内容。
    • rw”(读写模式):使用读写模式打开文件,允许对文件进行读取和写入操作,并且可以修改文件内容。
    • rws”(同步读写模式):使用同步读写模式打开文件,除了具有读写模式的功能外,还要求每次写入操作都要将数据同步刷新到底层存储介质(如硬盘)。在使用 rws 模式打开文件时,每次进行写入操作时,不仅会将数据写入到内存缓冲区,还会立即将数据刷新到底层存储介质(如硬盘)。这样可以保证数据的持久性,并且在发生系统崩溃或断电等异常情况时,数据不会丢失。由于每次写入操作都要进行磁盘刷新,所以相比于 rwd 模式,rws 模式的写入速度可能较慢。
    • rwd”(同步写模式):使用同步写模式打开文件,类似于同步读写模式,在使用 rwd 模式打开文件时,每次进行写入操作时,只有将数据写入到内存缓冲区,而不会立即刷新到底层存储介质。只有在调用 close() 方法、显式调用 getFD().sync() 方法或关闭程序时,才会将数据刷新到存储介质。相比于 rws 模式,rwd 模式的写入速度可能稍快,因为不需要每次都进行磁盘刷新。

    适用场景说明:

    1. 如果文件只需要具有读操作,就使用 r 模式;
    2. 如果文件既需要读又需要写,同时对持久化没有要求,就可以用 rw 模式;
    3. 如果文件既需要读又需要写,同时对持久化有要求严格或者是读多写少的情况,推荐使用 rws 模式
    4. 如果文件既需要读又需要写,同时对持久化有要求不是很严格 或者是 读少写多,推荐使用 rwd 模式

1.2 RandomAccessFile家族体系

  • RandomAccessFile家族体系

    image-20230826185124272

    image-20230826185142134

    • java.io.DataOutputDataOutput 接口提供了写入基本数据类型的方法,用于将数据以二进制格式写入输出流。RandomAccessFile 类实现了 DataOutput 接口,因此可以使用该接口定义的方法写入数据。
    • java.io.DataInputDataInput 接口定义了读取基本数据类型的方法,用于从输入流中以二进制格式读取数据。RandomAccessFile 类实现了 DataInput 接口,因此可以使用该接口定义的方法读取数据。
    • java.io.CloseableCloseable 是一个可关闭的接口,表示实现了该接口的类具备关闭资源的能力。RandomAccessFile 类实现了 Closeable 接口,因此可以通过调用 close() 方法关闭文件。
    • java.lang.AutoCloseableAutoCloseable 是一个自动关闭的接口,在 Java 7 中引入。它扩展了 Closeable 接口,并要求实现类必须提供一个细化的 close() 方法。RandomAccessFile 实现了 AutoCloseable 接口,所以可以使用 try-with-resources 语句来自动关闭文件。

2、RandomAccessFile基本使用

2.1 RandomAccessFile常用API介绍

  • 构造方法

    • RandomAccessFile(String name, String mode):创建一个具有指定名称的RandomAccessFile对象,并根据指定的模式打开文件。模式可以是"r"(只读),“rw”(读写)等。
  • 读取方法

    • int read():从文件中读取一个字节并返回该字节的整数值。

    • int read(byte[] b):从文件中读取一定数量的字节并存储到字节数组b中,并返回实际读取的字节数。

    • int read(byte[] b, int off, int len):从文件中读取最多len个字节,并将其存储到字节数组b中,偏移量为off,并返回实际读取的字节数。

    • int skipBytes(int n):跳过指定字节读(相对位置

  • 写入方法

    • void write(int b):将一个字节写入文件。

    • void write(byte[] b):将字节数组b中的所有字节写入文件。

    • void write(byte[] b, int off, int len):将字节数组b中从偏移量off开始的len个字节写入文件。

  • 文件操作方法

    • boolean exists():判断文件是否存在。

    • void createNewFile():创建一个新文件。

    • boolean delete():删除文件。

    • boolean renameTo(File dest):将文件重命名为dest指定的文件名。

    • long getFilePointer():返回当前文件指针的位置。

    • void seek(long pos):设置文件指针的位置为pos。(绝对位置

  • 文件长度相关方法

    • long length():返回文件的长度(以字节为单位)。

    • void setLength(long newLength):设置文件的长度为newLength。

  • 关闭方法

    • void close():关闭该RandomAccessFile对象,释放相关资源

2.2 RandomAccessFile常用API演示

环境搭建

  • Step1:创建一个Maven工程

  • Step2:在src/main/resources目录下准备一个 data.txt 文件,文件中的内容是

    hello world!
    

示例一

演示read方法

    public static void main(String[] args) throws Exception {
        RandomAccessFile raf = new RandomAccessFile("./src/main/resources/data.txt", "r");
        // 读取一个字节,此时已经读取了 h,data.txt 还剩 ello world!没有读取
        raf.read();
        byte[] bytes = new byte[1024];
        // 将从data.txt中读取的数据转存到字节数组中
        int len = raf.read(bytes);
        // 由于在 raf.read(bytes) 之前执行过一个 raf.read() 方法了,所以此时字节数组中会直接跳过第一个字节的数组
        // 由于UTF-8编码英文一个字母占一个字节(中文占3个字节)所以最终结果回漏掉 data.txt 中的首字母
        System.out.println(new String(bytes, 0, len)); // ello world!
    }

备注:data.txt 中一个空格也算一个字节

示例二

演示skipBytes

    public static void main(String[] args) throws Exception {
        RandomAccessFile raf = new RandomAccessFile("./src/main/resources/data.txt", "r");
        // 读取一个字节,此时已经读取了 h,data.txt 还剩 ello world!没有读取
        raf.read();
        // 相对当前读取位置(也就是 ello world!)再跳过两个字节 e 和 l,此时 data.txt 还剩 lo world! 没有读取
        raf.skipBytes(2);
        byte[] bytes = new byte[1024];
        int len = raf.read(bytes);
        System.out.println(new String(bytes, 0, len)); // lo world!
    }

示例三

演示seek

    public static void main(String[] args) throws Exception {
        RandomAccessFile raf = new RandomAccessFile("./src/main/resources/data.txt", "r");
        // 读取一个字节,此时已经读取了 h,data.txt 还剩 ello world!没有读取
        raf.read();
        // 相对 data.txt 原始位置(也就是 hello world!)跳过2个字节,此时 data.txt 还剩 llo world! 没有读取
        raf.seek(2);
        byte[] bytes = new byte[1024];
        int len = raf.read(bytes);
        System.out.println(new String(bytes, 0, len)); // llo world!
    }

示例四

演示write

    public static void main(String[] args) throws Exception {
        RandomAccessFile raf = new RandomAccessFile("./src/main/resources/data.txt", "rw");
        // 此时直接调用write方法,是从文件的第一个字节开始写, data.txt 变成了 ghplo world!
        // 同时此时指针也会随着写操作来到了 l
        raf.write("ghp".getBytes(StandardCharsets.UTF_8));
        // raf.seek(0);
        byte[] bytes = new byte[1024];
        int len = raf.read(bytes);
        // 由于之前的写操作,导致指针来到了 l,所以读操作从 l 开始读,所以最终读取的结果是 lo world!
        // 并不会读取到之前写入的数据
        System.out.println(new String(bytes, 0, len)); // lo world!
    }

备注:想要写入之后能够读取到 data.txt 中完整的数据,可以在执行完写操作之后,执行seek(0)将指针重置为初始位置,注意如果使用skipBytes(0)是没有效果的,因为它是相对位置

示例五

比较 原始输入输出流单线程拷贝大文件 和 RandomAccessFile实现多线程拷贝大文件

package com.ghp.file.test;

import java.io.*;
import java.util.concurrent.CountDownLatch;

/**
 * @author ghp
 * @title
 * @description
 */
public class Main {
    public static void main(String[] args) throws Exception {
        long startTime = System.currentTimeMillis();
//        copyFileBySingleThread(); // 单线程拷贝 476MB 的视频耗时 6903ms
        copyFileByMultiThread(); // 多线程拷贝 476MB 的视频耗时 3022ms
        long endTime = System.currentTimeMillis();
        System.out.println("文件拷贝耗时: " + (endTime - startTime) + "ms");

    }

    private static void copyFileBySingleThread() throws IOException {
        File file = new File("./src/main/resources/data.mp4");
        FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream("./src/main/resources/data-bak1.mp4");
        byte[] bytes = new byte[1024];
        int len = -1;
        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }
    }

    private static void copyFileByMultiThread() throws Exception {
        File file = new File("./src/main/resources/data.mp4");
        int threadNum = 5;
        // 计算每个线程需要读取的字节大小
        int part = (int) Math.ceil(file.length() / threadNum);
        // 创建线程计数器对象,用于阻塞主线程
        CountDownLatch latch = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
            final int k = i;
            new Thread(() -> {
                try {
                    RandomAccessFile fis = new RandomAccessFile(file, "r");
                    RandomAccessFile fos = new RandomAccessFile("./src/main/resources/data-bak.mp4", "rw");
                    // 设置读和写的位置
                    fis.seek(k * part);
                    fos.seek(k * part);
                    byte[] bytes = new byte[1024];
                    int sum = 0;
                    while (true) {
                        int len = fis.read(bytes);
                        if (len == -1){
                            // 文件已经读完了
                            break;
                        }
                        sum += len;
                        fos.write(bytes, 0, len);
                        if (sum >= part){
                            // 当前线程需要读取的字节已经读完了
                            break;
                        }
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 子线程执行完毕,线程计数器减一
                    latch.countDown();
                }
            }).start();
        }
        // 阻塞主线程,只有线程计数器归0,主线程才会继续执行
        latch.await();
    }

}

注意点

  1. 文件的切分必须是向上取整,否则回存在数据遗漏。向上取整能够保证即使多读了数据也会被覆盖避免数据遗漏,不会发生数据堆叠,而如果是向下取整就会导致数据遗漏
  2. 当前线程是否已经将自己那部分字节读取完毕的判断操作要在写操作之后,这样能够防止数据遗漏,判断操作放在写操作的后面,即使多写了数据,会被覆盖掉

2.3 RandomAccessFile实现断点续传

/**
     * 断点续传
     *
     * @param src       源文件(需要拷贝的文件)
     * @param target    目标文件(拷贝后的文件)
     * @param threadNum 线程数
     */
    private static void breakpointContinuation(File src, File target, int threadNum) throws Exception {
        // 每一个线程平均需要读取的字节数
        final int part = (int) Math.ceil(src.length() / threadNum);
        // 创建应该HashMap,用于记录每一个线程已读取的位置
        final Map<Integer, Integer> map = new ConcurrentHashMap<>();
        // 读取日志文件中的数据
        String[] logDatas = null;
        String logName = target.getCanonicalPath() + ".log";
        File logFile = new File(logName);
        if (logFile.exists()) {
            // 日志文件存在,则从上一次读取的位置开始读
            try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) {
                String data = reader.readLine();
                logDatas = data.split(",");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        final String[] logData = logDatas;
        CountDownLatch latch = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
            final int k = i;
            new Thread(() -> {
                try (RandomAccessFile in = new RandomAccessFile(src, "r");
                     RandomAccessFile out = new RandomAccessFile(target, "rw");
                     RandomAccessFile log = new RandomAccessFile(logName, "rw")) {
                    // 从指定位置读
                    int start = logData == null ? k * part : Integer.parseInt(logData[k]);
                    in.seek(start);
                    out.seek(start);
                    byte[] bytes = new byte[1024 * 2];
                    int sum = 0;
                    while (true) {
                        int len = in.read(bytes);
                        if (len == -1) {
                            // 文件所有字节已读完,结束读取
                            break;
                        }
                        sum += len;
                        // 记录当前线程已读取的位置
                        map.put(k, sum + start);
                        // 将读取到的数据、进行写入
                        out.write(bytes, 0, len);
                        // 将 map 中的数据持久化
                        log.seek(0);
                        StringJoiner joiner = new StringJoiner(",");
                        map.forEach((key, val) -> joiner.add(String.valueOf(val)));
                        log.write(joiner.toString().getBytes(StandardCharsets.UTF_8));
                        if (sum + (start) >= (1 + k) * part) {
                            // 当前线程读取的字节数量已经够了,结束读取
                            break;
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            }).start();
        }
        latch.await();
        // 读取完成后、将日志文件删除即可
        new File(logName).delete();
    }

参考文章

  • RandomAccessFile详解_江南煮酒的博客-CSDN博客
  • Java.io.RandomAccessFile 类 (w3schools.cn)
  • RandomAccessFile 解决多线程下载及断点续传-腾讯云开发者社区-腾讯云 (tencent.com)# RandomAccessFile学习笔记

1、RandomAccessFile基本介绍

1.1 RandomAccessFile相关基本概念

  • RandomAccessFile是什么

    RandomAccessFile 是 Java 中用于对文件进行随机访问的类。与普通的输入输出流不同,RandomAccessFile 允许在文件中任意位置读写数据。

  • RandomAccessFile的作用有哪些

    • 随机访问:与其他输入输出流不同,RandomAccessFile 允许在文件中任意位置进行读写操作,可以自由地定位文件指针。
    • 读写基本数据类型RandomAccessFile 实现了 DataInputDataOutput 接口,提供了方便的方法用于读取和写入基本数据类型。
    • 支持读写字节数组:除了读写基本数据类型外,还可以通过 read(byte[] buffer)write(byte[] buffer) 方法来读写字节数组。
    • 支持文件截断:可以使用 setLength(long newLength) 方法来调整文件的长度,将文件截断或扩展为指定的大小。

    常见的应用场景是使用 RandomAccessFile 实现断点续传

    注意事项RandomAccessFile只能读写文件(也就是字节数组)类型的数据,并不能读写流类型的数据

  • RandomAccessFile的四种访问模式

    访问模式特点
    r(read,只读模式)能读文件,不能写文件、持久化
    rw(read write,读写模式)能读、写文件,不能持久化
    rws(read write sync,同步读写模式)能读、写文件,每次写操作都会持久化
    rwd(read write data,同步写模式)能读、写文件,只有在调用close()、getFD().sync()、关闭程序时才进行持久化
    • r”(只读模式):使用只读模式打开文件,只能对文件进行读取操作,无法修改文件内容。
    • rw”(读写模式):使用读写模式打开文件,允许对文件进行读取和写入操作,并且可以修改文件内容。
    • rws”(同步读写模式):使用同步读写模式打开文件,除了具有读写模式的功能外,还要求每次写入操作都要将数据同步刷新到底层存储介质(如硬盘)。在使用 rws 模式打开文件时,每次进行写入操作时,不仅会将数据写入到内存缓冲区,还会立即将数据刷新到底层存储介质(如硬盘)。这样可以保证数据的持久性,并且在发生系统崩溃或断电等异常情况时,数据不会丢失。由于每次写入操作都要进行磁盘刷新,所以相比于 rwd 模式,rws 模式的写入速度可能较慢。
    • rwd”(同步写模式):使用同步写模式打开文件,类似于同步读写模式,在使用 rwd 模式打开文件时,每次进行写入操作时,只有将数据写入到内存缓冲区,而不会立即刷新到底层存储介质。只有在调用 close() 方法、显式调用 getFD().sync() 方法或关闭程序时,才会将数据刷新到存储介质。相比于 rws 模式,rwd 模式的写入速度可能稍快,因为不需要每次都进行磁盘刷新。

    适用场景说明:

    1. 如果文件只需要具有读操作,就使用 r 模式;
    2. 如果文件既需要读又需要写,同时对持久化没有要求,就可以用 rw 模式;
    3. 如果文件既需要读又需要写,同时对持久化有要求严格或者是读多写少的情况,推荐使用 rws 模式
    4. 如果文件既需要读又需要写,同时对持久化有要求不是很严格 或者是 读少写多,推荐使用 rwd 模式

1.2 RandomAccessFile家族体系

  • RandomAccessFile家族体系

    image-20230826185124272

    image-20230826185142134

    • java.io.DataOutputDataOutput 接口提供了写入基本数据类型的方法,用于将数据以二进制格式写入输出流。RandomAccessFile 类实现了 DataOutput 接口,因此可以使用该接口定义的方法写入数据。
    • java.io.DataInputDataInput 接口定义了读取基本数据类型的方法,用于从输入流中以二进制格式读取数据。RandomAccessFile 类实现了 DataInput 接口,因此可以使用该接口定义的方法读取数据。
    • java.io.CloseableCloseable 是一个可关闭的接口,表示实现了该接口的类具备关闭资源的能力。RandomAccessFile 类实现了 Closeable 接口,因此可以通过调用 close() 方法关闭文件。
    • java.lang.AutoCloseableAutoCloseable 是一个自动关闭的接口,在 Java 7 中引入。它扩展了 Closeable 接口,并要求实现类必须提供一个细化的 close() 方法。RandomAccessFile 实现了 AutoCloseable 接口,所以可以使用 try-with-resources 语句来自动关闭文件。

2、RandomAccessFile基本使用

2.1 RandomAccessFile常用API介绍

  • 构造方法

    • RandomAccessFile(String name, String mode):创建一个具有指定名称的RandomAccessFile对象,并根据指定的模式打开文件。模式可以是"r"(只读),“rw”(读写)等。
  • 读取方法

    • int read():从文件中读取一个字节并返回该字节的整数值。

    • int read(byte[] b):从文件中读取一定数量的字节并存储到字节数组b中,并返回实际读取的字节数。

    • int read(byte[] b, int off, int len):从文件中读取最多len个字节,并将其存储到字节数组b中,偏移量为off,并返回实际读取的字节数。

    • int skipBytes(int n):跳过指定字节读(相对位置

  • 写入方法

    • void write(int b):将一个字节写入文件。

    • void write(byte[] b):将字节数组b中的所有字节写入文件。

    • void write(byte[] b, int off, int len):将字节数组b中从偏移量off开始的len个字节写入文件。

  • 文件操作方法

    • boolean exists():判断文件是否存在。

    • void createNewFile():创建一个新文件。

    • boolean delete():删除文件。

    • boolean renameTo(File dest):将文件重命名为dest指定的文件名。

    • long getFilePointer():返回当前文件指针的位置。

    • void seek(long pos):设置文件指针的位置为pos。(绝对位置

  • 文件长度相关方法

    • long length():返回文件的长度(以字节为单位)。

    • void setLength(long newLength):设置文件的长度为newLength。

  • 关闭方法

    • void close():关闭该RandomAccessFile对象,释放相关资源

2.2 RandomAccessFile常用API演示

环境搭建

  • Step1:创建一个Maven工程

  • Step2:在src/main/resources目录下准备一个 data.txt 文件,文件中的内容是

    hello world!
    

示例一

演示read方法

    public static void main(String[] args) throws Exception {
        RandomAccessFile raf = new RandomAccessFile("./src/main/resources/data.txt", "r");
        // 读取一个字节,此时已经读取了 h,data.txt 还剩 ello world!没有读取
        raf.read();
        byte[] bytes = new byte[1024];
        // 将从data.txt中读取的数据转存到字节数组中
        int len = raf.read(bytes);
        // 由于在 raf.read(bytes) 之前执行过一个 raf.read() 方法了,所以此时字节数组中会直接跳过第一个字节的数组
        // 由于UTF-8编码英文一个字母占一个字节(中文占3个字节)所以最终结果回漏掉 data.txt 中的首字母
        System.out.println(new String(bytes, 0, len)); // ello world!
    }

备注:data.txt 中一个空格也算一个字节

示例二

演示skipBytes

    public static void main(String[] args) throws Exception {
        RandomAccessFile raf = new RandomAccessFile("./src/main/resources/data.txt", "r");
        // 读取一个字节,此时已经读取了 h,data.txt 还剩 ello world!没有读取
        raf.read();
        // 相对当前读取位置(也就是 ello world!)再跳过两个字节 e 和 l,此时 data.txt 还剩 lo world! 没有读取
        raf.skipBytes(2);
        byte[] bytes = new byte[1024];
        int len = raf.read(bytes);
        System.out.println(new String(bytes, 0, len)); // lo world!
    }

示例三

演示seek

    public static void main(String[] args) throws Exception {
        RandomAccessFile raf = new RandomAccessFile("./src/main/resources/data.txt", "r");
        // 读取一个字节,此时已经读取了 h,data.txt 还剩 ello world!没有读取
        raf.read();
        // 相对 data.txt 原始位置(也就是 hello world!)跳过2个字节,此时 data.txt 还剩 llo world! 没有读取
        raf.seek(2);
        byte[] bytes = new byte[1024];
        int len = raf.read(bytes);
        System.out.println(new String(bytes, 0, len)); // llo world!
    }

示例四

演示write

    public static void main(String[] args) throws Exception {
        RandomAccessFile raf = new RandomAccessFile("./src/main/resources/data.txt", "rw");
        // 此时直接调用write方法,是从文件的第一个字节开始写, data.txt 变成了 ghplo world!
        // 同时此时指针也会随着写操作来到了 l
        raf.write("ghp".getBytes(StandardCharsets.UTF_8));
        // raf.seek(0);
        byte[] bytes = new byte[1024];
        int len = raf.read(bytes);
        // 由于之前的写操作,导致指针来到了 l,所以读操作从 l 开始读,所以最终读取的结果是 lo world!
        // 并不会读取到之前写入的数据
        System.out.println(new String(bytes, 0, len)); // lo world!
    }

备注:想要写入之后能够读取到 data.txt 中完整的数据,可以在执行完写操作之后,执行seek(0)将指针重置为初始位置,注意如果使用skipBytes(0)是没有效果的,因为它是相对位置

示例五

比较 原始输入输出流单线程拷贝大文件 和 RandomAccessFile实现多线程拷贝大文件

package com.ghp.file.test;

import java.io.*;
import java.util.concurrent.CountDownLatch;

/**
 * @author ghp
 * @title
 * @description
 */
public class Main {
    public static void main(String[] args) throws Exception {
        long startTime = System.currentTimeMillis();
//        copyFileBySingleThread(); // 单线程拷贝 476MB 的视频耗时 6903ms
        copyFileByMultiThread(); // 多线程拷贝 476MB 的视频耗时 3022ms
        long endTime = System.currentTimeMillis();
        System.out.println("文件拷贝耗时: " + (endTime - startTime) + "ms");

    }

    private static void copyFileBySingleThread() throws IOException {
        File file = new File("./src/main/resources/data.mp4");
        FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream("./src/main/resources/data-bak1.mp4");
        byte[] bytes = new byte[1024];
        int len = -1;
        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }
    }

    private static void copyFileByMultiThread() throws Exception {
        File file = new File("./src/main/resources/data.mp4");
        int threadNum = 5;
        // 计算每个线程需要读取的字节大小
        int part = (int) Math.ceil(file.length() / threadNum);
        // 创建线程计数器对象,用于阻塞主线程
        CountDownLatch latch = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
            final int k = i;
            new Thread(() -> {
                try {
                    RandomAccessFile fis = new RandomAccessFile(file, "r");
                    RandomAccessFile fos = new RandomAccessFile("./src/main/resources/data-bak.mp4", "rw");
                    // 设置读和写的位置
                    fis.seek(k * part);
                    fos.seek(k * part);
                    byte[] bytes = new byte[1024];
                    int sum = 0;
                    while (true) {
                        int len = fis.read(bytes);
                        if (len == -1){
                            // 文件已经读完了
                            break;
                        }
                        sum += len;
                        fos.write(bytes, 0, len);
                        if (sum >= part){
                            // 当前线程需要读取的字节已经读完了
                            break;
                        }
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 子线程执行完毕,线程计数器减一
                    latch.countDown();
                }
            }).start();
        }
        // 阻塞主线程,只有线程计数器归0,主线程才会继续执行
        latch.await();
    }

}

注意点

  1. 文件的切分必须是向上取整,否则回存在数据遗漏。向上取整能够保证即使多读了数据也会被覆盖避免数据遗漏,不会发生数据堆叠,而如果是向下取整就会导致数据遗漏
  2. 当前线程是否已经将自己那部分字节读取完毕的判断操作要在写操作之后,这样能够防止数据遗漏,判断操作放在写操作的后面,即使多写了数据,会被覆盖掉

2.3 RandomAccessFile实现断点续传

/**
     * 断点续传
     *
     * @param src       源文件(需要拷贝的文件)
     * @param target    目标文件(拷贝后的文件)
     * @param threadNum 线程数
     */
    private static void breakpointContinuation(File src, File target, int threadNum) throws Exception {
        // 每一个线程平均需要读取的字节数
        final int part = (int) Math.ceil(src.length() / threadNum);
        // 创建应该HashMap,用于记录每一个线程已读取的位置
        final Map<Integer, Integer> map = new ConcurrentHashMap<>();
        // 读取日志文件中的数据
        String[] logDatas = null;
        String logName = target.getCanonicalPath() + ".log";
        File logFile = new File(logName);
        if (logFile.exists()) {
            // 日志文件存在,则从上一次读取的位置开始读
            try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) {
                String data = reader.readLine();
                logDatas = data.split(",");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        final String[] logData = logDatas;
        CountDownLatch latch = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
            final int k = i;
            new Thread(() -> {
                try (RandomAccessFile in = new RandomAccessFile(src, "r");
                     RandomAccessFile out = new RandomAccessFile(target, "rw");
                     RandomAccessFile log = new RandomAccessFile(logName, "rw")) {
                    // 从指定位置读
                    int start = logData == null ? k * part : Integer.parseInt(logData[k]);
                    in.seek(start);
                    out.seek(start);
                    byte[] bytes = new byte[1024 * 2];
                    int sum = 0;
                    while (true) {
                        int len = in.read(bytes);
                        if (len == -1) {
                            // 文件所有字节已读完,结束读取
                            break;
                        }
                        sum += len;
                        // 记录当前线程已读取的位置
                        map.put(k, sum + start);
                        // 将读取到的数据、进行写入
                        out.write(bytes, 0, len);
                        // 将 map 中的数据持久化
                        log.seek(0);
                        StringJoiner joiner = new StringJoiner(",");
                        map.forEach((key, val) -> joiner.add(String.valueOf(val)));
                        log.write(joiner.toString().getBytes(StandardCharsets.UTF_8));
                        if (sum + (start) >= (1 + k) * part) {
                            // 当前线程读取的字节数量已经够了,结束读取
                            break;
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            }).start();
        }
        latch.await();
        // 读取完成后、将日志文件删除即可
        new File(logName).delete();
    }

参考文章

  • RandomAccessFile详解_江南煮酒的博客-CSDN博客
  • Java.io.RandomAccessFile 类 (w3schools.cn)
  • RandomAccessFile 解决多线程下载及断点续传-腾讯云开发者社区-腾讯云 (tencent.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/195627.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

cadence virtuoso simulation文件夹删除

ADE XL仿真结果错误&#xff0c;与预期结果差别太大&#xff0c;与ADE L仿真结果也差别很大。 可能是由于仿真数据过多&#xff0c;卡爆了。 在virtuoso启动路径下&#xff0c;simulation文件夹是仿真过程文件&#xff0c;可以将此文件夹清空。 清空后ADE XL仿真结果正常了。…

Snagit 2024.0.1(Mac屏幕截图软件)

Snagit 2024是一款屏幕截图工具&#xff0c;可以帮助用户轻松捕获、编辑和分享屏幕截图。该工具在Mac上运行&#xff0c;旨在满足用户对于屏幕截图的各种需求。 Snagit 2024支持屏幕录制功能&#xff0c;可以录制摄像头和麦克风等外部设备&#xff0c;让用户录制更加全面的视频…

vue3中toRef创建一个ref对象

为源响应式对象上的某个属性创建一个 ref对象, 二者内部操作的是同一个数据值, 更新时二者是同步的 区别ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响 应用: 当要将 某个prop 的 ref 传递给复合函数时&#xff0c;toRef 很有用 父组件代码: <template><…

“PredictingChildrenHeight“ app Tech Support(URL)

Using our app, we can predict a childs height through formulas. Because there are many factors that affect a childs height, it is for reference only. ​​​​​​​ If you have any questions, you can either leave a message or send the questions to our em…

零基础在ubuntu上搭建rtmp服务器-srs

搭建服务器 搭建 SRS&#xff08;Simple-RTMP-Server&#xff09;服务器需要一些步骤&#xff0c;以下是一个简单的步骤指南。请注意&#xff0c;SRS 的配置可能会有所不同&#xff0c;具体取决于你的需求和环境。在开始之前&#xff0c;请确保你的 Ubuntu 系统已经连接到互联…

Nacos 端口偏移量说明

因为安全原因&#xff0c;在部署nacos-2.2.3版本时&#xff0c;将nacos的application.properties中的server.port端口值由默认值8848改成了server.port8425 问题&#xff1a;nacos 启动时(sh start.sh -m standalone)报错 如下&#xff1a; 经过分析&#xff0c;原因是 9425 …

每日汇评:原油价格正在等待欧佩克对2024年供应削减配额的决定

OPEC会议推迟至周四&#xff0c;个别配额和供应削减仍然是会议的核心议题&#xff1b; 原油价格在欧佩克会议前持平&#xff0c;但是否有意外的看涨取决于欧佩克的减产&#xff1b; 布伦特原油价格在关键的82美元和200均线的交叉点被明显拒绝后走低&#xff1b; 上周三&#xf…

救助儿童会携手联劝公益在世界儿童日举办系列活动

2023年11月17日&#xff0c;为了呼吁家长关注对儿童情绪的关注与表达&#xff0c;救助儿童会携手联劝公益&#xff0c;以“走出情绪迷宫”为主题&#xff0c;于南丰城举办线下公益展&#xff0c;呼吁关注儿童心理健康。 本次展览由情绪迷宫、儿童艺术疗育活动、芝麻街互动三部分…

Docker-compose容器编排

Docker-compose容器编排 是什么 Compose 是 Docker 公司推出的一个工具软件&#xff0c;可以管理多个 Docker 容器组成一个应用。你需要定义一个 YAML 格式的配置文件docker-compose.yml&#xff0c;写好多个容器之间的调用关系。然后&#xff0c;只要一个命令&#xff0c;就…

Doris-Routine Load(二十七)

例行导入&#xff08;Routine Load&#xff09;功能为用户提供了一种自动从指定数据源进行数据导入的功能。 适用场景 当前仅支持从 Kafka 系统进行例行导入&#xff0c;使用限制&#xff1a; &#xff08;1&#xff09;支持无认证的 Kafka 访问&#xff0c;以及通过 SSL 方…

解析直播第三方美颜SDK:技术原理与应用

时下&#xff0c;直播平台和主播们纷纷引入美颜技术&#xff0c;以提升视觉效果和用户体验。而在众多美颜技术中&#xff0c;直播第三方美颜SDK成为许多开发者和平台的首选&#xff0c;因其灵活性和高效性而备受推崇。 一、技术原理&#xff1a;美颜算法的精髓 第三方美颜SDK…

02-Java集合之双列集合,如HashMap,Hashtable,Properties,TreeMap的底层结构

双列集合 添加/获取/删除键值对原理 哈希表/散列表是一种将数组和单向链表融合在一起的数据结构 数组在查询方面效率很高,单向链表在随机增删方面效率较高,哈希表将以上的两种数据结构融合在一起后充分发挥它们各自的优点 双列集合以key和value这种键值对方式存储数据: key…

ViLT 论文精读【论文精读】

ViLT 论文精读【论文精读】_哔哩哔哩_bilibili 目录 ViLT 论文精读【论文精读】_哔哩哔哩_bilibili 1 地位 2 ViLT做了什么能让它成为这种里程碑式的工作&#xff1f; 3 ViLT到底把模型简化到了什么程度&#xff1f;到底能加速到什么程度&#xff1f; 2.1 过去的方法是怎…

bop数据合并到COCO

bop数据合并到COCO JSON转TXT重命名txt文件中类别信息的转换 JSON转TXT import json import os,globcategories [{"id": 12,"name": "OREO","supercategory": "icbin"},{"id": 16,"name": "Paper…

Rust语言入门教程(七) - 所有权系统

所有权系统是Rust敢于声称自己为一门内存安全语言的底气来源&#xff0c;也是让Rust成为一门与众不同的语言的所在之处。也正是因为这个特别的所有权系统&#xff0c;才使得编译器能够提前暴露代码中的错误&#xff0c;并给出我们必要且精准的错误提示。 所有权系统的三个规则…

微信小程序生成二维码并保存到本地方法

微信小程序生成二维码请保存到本地方法 官方weapp-qrcode插件 github链接 功能完成样子 wxml <view class"qrcode"><canvas style"width: 275px; height: 275px;" canvas-idmyQrcode></canvas> </view> <view class" …

Transformers实战——文本相似度

文章目录 一、改写文本分类1.导入相关包2.加载数据集3.划分数据集4.数据集预处理5.创建模型6.创建评估函数7.创建 TrainingArguments8.创建 Trainer9.模型训练10.模型评估11.模型预测 二、交互/单塔模式1.导入相关包2.加载数据集3.划分数据集4.数据集预处理5.创建模型&#xff…

机器学习的复习笔记2-回归

一、什么是回归 机器学习中的回归是一种预测性分析任务&#xff0c;旨在找出因变量&#xff08;目标变量&#xff09;和自变量&#xff08;预测变量&#xff09;之间的关系。与分类问题不同&#xff0c;回归问题关注的是预测连续型或数值型数据&#xff0c;如温度、年龄、薪水…

springboot自定义更换启动banner动画

springboot自定义更换启动banner动画 文章目录 springboot自定义更换启动banner动画 &#x1f4d5;1.新建banner&#x1f5a5;️2.启动项目&#x1f516;3.自动生成工具&#x1f9e3;4.彩蛋 &#x1f58a;️最后总结 &#x1f4d5;1.新建banner 在resources中新建banner.txt文…

数智赋能 锦江汽车携手苏州金龙打造高质量盛会服务

作为一家老牌客运公司&#xff0c;成立于1956年的上海锦江汽车服务有限公司&#xff08;以下简称锦江汽车&#xff09;&#xff0c;拥有1200多辆大巴和5000多辆轿车&#xff0c;是上海乃至长三角地区规模最大的专业旅游客运公司。面对客运市场的持续萎缩&#xff0c;锦江汽车坚…