提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- RandomAccessFile
- 1.简介
- 2.RandomAccessFile的作用有哪些?
- 3.RandomAccessFile类有两个构造器:
- 3.常用API介绍
- RandomAccessFile 案例
- 环境搭建
- 1.read方法
- 2.skipBytes方法
- 3.seek方法
- 4.write方法
- RandomAccessFile 实现多线程拷贝大文件
- 1.多线程拷贝
- 2.断点续传
RandomAccessFile
1.简介
- RandomAccessFile是Java 输入/输出流体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出数据。
- 与普通的输入/输出流不同的是,RandomAccessFile支持"随机访问"的方式,程序可以直接跳转到文件的任意地方来读写数据。
2.RandomAccessFile的作用有哪些?
3.RandomAccessFile类有两个构造器:
- RandomAccessFile(String name, String mode):
- RandomAccessFile(File file, String mode)
创建一个随机访问文件的流:
(1)构造器1中name会转换为构造器2中的file,RandomAccessFile(String name, String mode)等价于RandomAccessFile(new File(name), String mode)
(2)mode – 访问模式
3.常用API介绍
RandomAccessFile 案例
环境搭建
1.read方法
java"> 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 中一个空格也算一个字节
2.skipBytes方法
java"> 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!}
3.seek方法
java"> 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!}
4.write方法
java"> public static void main(String[] args) throws Exception {RandomAccessFile raf = new RandomAccessFile("./src/main/resources/data.txt", "rw");// 此时直接调用write方法,是从文件的第一个字节开始写, data.txt 变成了 ghplo world!// 同时此时指针也会随着写操作来到了 lraf.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 实现多线程拷贝大文件
1.多线程拷贝
- 比较 原始输入输出流单线程拷贝大文件 和 RandomAccessFile实现多线程拷贝大文件
java">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 的视频耗时 6903mscopyFileByMultiThread(); // 多线程拷贝 476MB 的视频耗时 3022mslong 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();}}
注意点:
- 文件的切分必须是向上取整,否则回存在数据遗漏。向上取整能够保证即使多读了数据也会被覆盖避免数据遗漏,不会发生数据堆叠,而如果是向下取整就会导致数据遗漏
- 当前线程是否已经将自己那部分字节读取完毕的判断操作要在写操作之后,这样能够防止数据遗漏,判断操作放在写操作的后面,即使多写了数据,会被覆盖掉
2.断点续传
java">/*** 断点续传** @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();}