多线程复制文件,实现断点续传

想要的效果:把文件分成 n 份,开启 n 个线程,每个线程负责复制一份文件。当程序中断后,再次开启是不需要重新复制,而是从已经完成的地方开始复制。

思路

  • 单线程的断点续传,每一次复制前先获得目标文件已经复制的长度,根据已复制的长度设置偏移量开始复制。
  • 多线程的断点续传问题在于,已经复制的内容不是连续的,获取目标文件的长度获得的是总长度,不能获得每个线程单独已经复制的长度。
  • 解决方法: 用一个文本文档记录每个线程每一次复制的偏移量(获得偏移量等于获得已复制长度),下一次再复制时,直接读取该文档,获取该线程已经完成的偏移量,直接从该偏移量开始继续复制。

优化

  1. 根据上面思路,需要多一个记录文档的文件流,一开始打算用的是 ObjectOutputStream对象文件流,存储一个 Map 对象;或者是 BufferedReader 缓冲区字符流,存储 字符串,读写时再转换成普通的 Map 对象。

  2. 实际操作中,需要在一开始判断记录文档 不存在 或者 长度为0 时,创建一个文档,把 所有初始数据(index + index对应的偏移量) 先写入文档中。这样当多线程开始启动时,确保每个线程都能获得初始的偏移量。

  3. 可是这两种做法每次在读写时都需要进行转换数据格式,除此之外还会有 更新丢失 的问题:当线程A正在复制数据时,先获取了所有index的偏移量(包含线程B),此时线程B也在复制,复制完成后把新的偏移量写入文档,这时候线程A才复制完成,把偏移量写入文档,但是线程A的偏移量记录中记录的是线程B旧的偏移量,会把线程B刚刚新完成的偏移量覆盖了,导致 “复制失败(成功了但没完全成功)”

  4. 更新失败的解决方法有两个:

    • 添加 同步锁,多线程操作时,让每次只能有一个线程对记录文档进行读写操作。
    • 因为每次某个线程操作时,都会把文档中所有数据当成一个对象来处理,读取的时候没有问题,但是修改时,明明只是修改了只属于自己的一部分,却需要修改整个对象,即对所有线程数据进行操作。这是不合理的。所以需要 把文档中的数据拆成多份,每个线程只能获取并修改属于自己的数据,而且不会影响到其他的数据。
  5. 根据以上分析,决定使用 RandomAccessFile 随机访问流 来记录偏移量。RandomAccessFile 随机访问流 每次读写操作都可以 设置文件指针偏移 来进行,可以只读写部分数据,而不会影响到别的数据。由于文档是用来记录偏移量的,偏移量用的是 long 类型数据,一个 long 类型数据占8个字节。可以设置一个固定长度为 8 * 线程数的文档,每8个字节代表了一组偏移量数据。例如第2组数据是在第(2-1) * 8的偏移量开始记录,第5组数据是在第(5-1) * 8的偏移量开始记录.

最终代码

public static void main(String[] args) throws IOException {
    // 源文件
    File inputF = new File("E:\\read.mp4");
    // 目标文件
    File outputF = new File("E:\\write.mp4");
    // 记录偏移量文件
    File offsetF = new File("E:\\log.txt");
    // 线程数量
    int threadNum = 10;
    // 每条线程需要读写长度
    long copySize = inputF.length() / threadNum;

    // 判断是否记录文件是否存储了数据
    RandomAccessFile logReader = new RandomAccessFile(offsetF, "rw");
    // 长度为0即没有数据,初始化长度,long类型数据长度为8,那文件的总长度就是 线程数*8
    if (logReader.length() == 0) {
        logReader.setLength(threadNum * 8);
    }

    // 开启多个线程,由于文件长度未必能线程数被整除,所以最后一个线程需要处理的长度需要单独拿出来计算
    for (int i = 0; i < threadNum - 1; i++) {
        // 线程构造方法的参数为:源文件,目标文件,起始偏移量,复制后应该到达偏移量,index(对应负责的区域)
        new MyThread(inputF, outputF, offsetF, copySize * i, copySize * (i + 1), i).start();
    }
    new MyThread(inputF, outputF, offsetF, copySize * (threadNum - 1), inputF.length(), threadNum - 1).start();
    
    logReader.close();
}

MyThread 类

// 源文件
private File inputF;
// 目标文件
private File outputF;
// 记录偏移量文件
private File offsetF;
// 该线程复制的起始偏移量
private long start;
// 该线程复制的末尾偏移量
private long end;
// 该线程负责的区域
private int index;

@Override
public void run() {
    // 文件流
    RandomAccessFile inputStream = null;
    RandomAccessFile outputStream = null;
    RandomAccessFile offsetStream = null;

    try {
        inputStream = new RandomAccessFile(inputF, "r");
        outputStream = new RandomAccessFile(outputF, "rw");
        offsetStream = new RandomAccessFile(offsetF, "rw");

        // 从记录文件中偏移量 index * 8 的位置开始,记录的是该线程的偏移量
        offsetStream.seek(index * 8);
        // 获取该线程到达的偏移量
        long offset = offsetStream.readLong();
        // 偏移量为0,即为第一次复制,初始化偏移量为默认偏移量
        if (offset == 0) {
            offset = start;
        }
        // 读写流偏移
        inputStream.seek(offset);
        outputStream.seek(offset);

        // 偏移量
        long pointer;
        
        // 文件流读写过程
        byte[] b = new byte[1024 * 5];
        int len;
        while ((len = inputStream.read(b)) != -1) {
            outputStream.write(b, 0 ,len);

            // 获取现在复制文件输出流的偏移量
            pointer = outputStream.getFilePointer();
            // 把记录文件流的偏移量设置回起点
            offsetStream.seek(index * 8);
            // 写入新的偏移量
            offsetStream.writeLong(pointer);

            // 判断偏移量是否已经超过应该到达的偏移量
            if (pointer >= end) {
                break;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 关流
        try {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
            if (offsetStream != null) {
                offsetStream.close();
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值