RandomAccessFile类 & 断点续传

学习链接

RandomAccessFile详解
Java IO——RandomAccessFile类详解
java多线程-断点续传

RandomAccessFile

构造方法

需要传入一个 File 和 指定模式

  • 当指定的file不存在时,会创建文件。但它不会帮我们创建目录,目录不存在的话,会报错。当指定的file存在时,不会对原文件有影响
  • 模式可以指定为:r、rw、rwd、rws

实现的接口

它实现了3个接口,其中的方法如下

DataOutput

定义了写的方法,每次写都是从文件指针的位置开始写,每写一个字节,文件指针往后移动一位,可以接着从文件指针位置继续读,初始文件指针filePointer为0,也就意味着刚开始如果不设置初始文件指针,就会从头开始覆盖文件的数据。

  • 与FileOutputStream区别:文件输出流如果需要追加,需要传入第二个参数append为true,否则,会删除文件的所有字节,这点是比较危险的。
  • RandomAccessFile可以先把文件指针移动最后面(通过seek或skipBytes),然后开始写入)
- void write(int b)-写入的是一个字符,而不是整数,写入整数要用writeInt
- void write(byte b[])
- void write(byte b[], int off, int len)
- void writeBoolean(boolean v)
- void writeByte(int v)
- void writeShort(int v)
- void writeChar(int v)
- void writeInt(int v)
- void writeLong(long v)
- void writeDouble(double v)
- void writeBytes(String s)
- void writeChars(String s)
- void writeUTF(String s)

DataInput

定义了读的方法,每次读都是从指针的位置开始写,每读一个字节,指针往后移动一位,可以接着从指针位置继续读,初始指针filePointer为0。

- void    readFully(byte b[], int off, int len) - 将从文件指针开始最多读取len个字节内容,给到byte数组的从off开始的偏移量
- void    readFully(byte b[]) - 同上,只不过off为0,len为b.length
- int     skipBytes(int n)
- boolean readBoolean()
- byte    readByte()
- int     readUnsignedByte()
- short   readShort()
- int     readUnsignedShort()
- char    readChar()
- int     readInt()
- long    readLong()
- float   readFloat()
- double  readDouble()
- String  readLine()
- String  readUTF()

AutoCloseable

- void close()

重要的方法

native long length()
    - 获取文件的字节数


native void setLength(long newLength)
    - 设置文件占用的字节数(如果源文件不够这个字节数量,用0补充;如果源文件比这个子节数量,要是多的话,多的部分直接截掉)


native long getFilePointer()
    - 获取当前文件指针(以字节为单位,相当于游标)


void seek(long pos)(绝对位置)
    - 可以直接用来设置当前文件指针的位置(会移动文件指针到指定的位置),从文件最开始的位置开始算
    - 可以超出文件大小,但是超出后,只有当写入之后,才会改变文件的大小


int skipBytes(int n) (相对位置)
    - 从当前位置跳过指定的字节数量(会移动文件指针到跳过的位置)
    - 如果设置的参数超过了文件末尾,不会抛出异常,只会返回实际跳过的字节数

多线程读写同一个文件(多线程复制文件)

代码1

代码是每个线程负责一个分片大小10M,算出需要创建的线程,然后每个线程就只负责自己的10M数据。


public class RafTest {
    
    
    public static void main(String[] args) throws IOException, InterruptedException {
    
    

        File file = new File("D:\\Projects\\demo-raf\\test.mp4");

        // 分片大小
        int shardSize = 10 * 1024 * 1024; // 10M

		// 线程数量
        int threadNum = (int) (Math.ceil((double) file.length() / shardSize));
        System.out.println("线程数量: " + threadNum);
        
        CountDownLatch latch = new CountDownLatch(threadNum);

        List<Thread> threadList = new ArrayList<>();
        
        for (int i = 0; i < threadNum; i++) {
    
    
            final int j =i;
            Thread t = new Thread(
                    () -> {
    
    
                        try {
    
    
                        
							// 每个线程要用自己的RandomAccessFile,它不是线程安全的
							
                            // 待读取的文件
                            RandomAccessFile rafsrc = new RandomAccessFile(new File("D:\\Projects\\demo-raf\\test.mp4"), "r");

                            // 待写入的文件
                            // (刚开始,这个文件是不存在的,new完之后,还没开始写,它就创建了,第一个线程可以直接写入)
                            // (第二个线程发现当前文件是存在的,然后需要从指定位置开始写,注意这个指定位置是可以超过文件大小的,看seek的用法)
                            RandomAccessFile raftarget = new RandomAccessFile(new File("D:\\Projects\\demo-raf\\test-copy.mp4"), "rw");

                            // 每个线程负责读写的开始位置
                            int pos = j * shardSize;
                            rafsrc.seek(pos);
                            raftarget.seek(pos);

                   
                            // 每个线程最多负责读取长度为shardSize,即一个分片大小
                            int totalRead = 0; // 记录当前线程已读取的字节数
                            int len = 0; // 当次循环中读取的字节数
   							
   							// 缓冲数组
                            byte[] bytes = new byte[5 * 1024]; // 5k

                            while (true) {
    
    

								// 最多读取到bytes.lengh的字节数量的数据到bytes中,len是读取的字节数量
                                len = rafsrc.read(bytes);

                                // 读到末尾了,没数据了,就退出循环
                                if (len == -1) {
    
    
                                    break;
                                }

								// 确定上面的read方法能读到数据,再写入
                                raftarget.write(bytes, 0, len);

                                totalRead += len; // 当前线程已读取并且写入的字节数

                                // 每个线程只负责一分片大小
                                if (totalRead >= shardSize) {
    
     // 1. 这里大于或等于的意思是:要读够一个分片大小,除非遇到文件末尾了
                                    break;                    // 2. 读取该分片时,由于缓冲数组的存在,有可能会读到下一个分片的数据,
                                }                             //    但是没有关系,下一个分片的起始位置会从指定处开始写,因此会覆盖上一个线程写入的数据。
                                                              //    覆不覆盖也没关系,都是同样的数据嘛(这点须理解下,由于缓冲数组的存在,多个线程可能操作了同一段数据了)
                            }                                 // 3. 读取到最后一个分片时,这个分片大小一定小于或等于预设值的分片大小,由于缓存数组的存在,会不会存在问题呢?
                                                              //    没有问题,因为最后一次读取都不够缓存数组的话,会返回已读取到的字节数,然后写入后,继续读取,就会返回-1,从而退出了循环
                                                              // 4. 线程的执行顺序也对最终写的文件没有影响,需要结合seek的用法来看,因为seek本身就能指定超过文件大小的位置,
                                                              //    然后从这个位置开始写入,因此,先写入前一段,还是后一段都没影响,这一点比较重要哦!也能从中体会到这个类的作用。
                            latch.countDown();

                        } catch (IOException e) {
    
    
                            e.printStackTrace();
                        }
                    }
            );
            threadList.add(t);
        }

        threadList.forEach(t->t.start());

        latch.await();

    }
}

代码2

上面这样写不好,最好改成成固定线程数量,每个线程平分,不然文件一大,创建的线程数量太多了。(详细解释请看代码1的注释)

package com.zzhua;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class RafTest2 {
    
    
    public static void main(String[] args) throws IOException, InterruptedException {
    
    

        File file = new File("D:\\documents\\尚硅谷JavaScript高级教程\\视频1.zip");

        int threadNum = 5; // 指定5个线程

        // 计算每个分片大小
        final int shardSize = (int)(Math.ceil((double)file.length() / threadNum));

        CountDownLatch latch = new CountDownLatch(threadNum);

        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < threadNum; i++) {
    
    
            final int j =i;
            Thread t = new Thread(
                    () -> {
    
    
                        try {
    
    

                            // 待读取的文件
                            RandomAccessFile rafsrc = new RandomAccessFile(new File("D:\\documents\\尚硅谷JavaScript高级教程\\视频1.zip"), "r");

                            // 待写入的文件
                            RandomAccessFile raftarget = new RandomAccessFile(new File("D:\\documents\\尚硅谷JavaScript高级教程\\视频1_copy.zip"), "rw");

                            // 每个线程负责读写的开始位置
                            int pos = j * shardSize;
                            rafsrc.seek(pos);
                            raftarget.seek(pos);

                            // 缓冲数组
                            byte[] bytes = new byte[5 * 1024]; // 5k

                            // 每个线程最多负责读取长度为shardSize,即一个分片大小
                            int totalRead = 0; // 记录当前线程已读取的字节数
                            int len = 0; // 当次循环中读取的字节数

                            while (true) {
    
    

                                len = rafsrc.read(bytes);

                                // 读到末尾了,没数据了,就退出循环
                                if (len == -1) {
    
    
                                    break;
                                }

                                raftarget.write(bytes, 0, len);

                                totalRead += len; // 当前线程已读取并且写入的字节数

                                // 每个线程只负责一分片大小
                                if (totalRead >= shardSize) {
    
    
                                    break;
                                }

                            }


                            latch.countDown();

                        } catch (IOException e) {
    
    
                            e.printStackTrace();
                        }
                    }
            );
            threadList.add(t);
        }

        threadList.forEach(t->t.start());

        latch.await();

    }
}

断点续传

FileUtils

以下代码的过程:大致与上面相同,但是添加了一个ConcurrentHashMap去记录 线程标识 => 当前线程完成写入的位置(位置是相对于文件起始位置开始计算的),当线程中的每次循环读取完成时,将当次循环读取的数量 加上 当前线程开始时的偏移量,这个偏移量在一切正常的情况下是:k * part,然后写入log日志。当某个时刻,程序终止,所有线程停止。这个文件就记录了,所有线程完成的情况,每个线程之前已经处理过的就不需要再处理了,而是接着从记录的位置开始读写即可,这样,每个线程不用从头开始,就实现了断点续传。

package com.qikux.utils;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;

public class FileUtils {
    
    
    /**
     * 支持断点续传
     * @src 拷贝的原文件
     * @desc 拷贝的位置
     * @threadNum 开启的线程数
     */
    public static void transportFile(File src, File desc, int threadNum) throws Exception {
    
    
        // 每一个线程读取的大小
        int part = (int)Math.ceil(src.length() / threadNum);
        // 存储多个线程、用于阻塞主线程
        List<Thread> list = new ArrayList<>();

        // 定义一个基于多线程 的 hashmap
        final Map<Integer, Integer> map = new ConcurrentHashMap<>();
        // 读取 日志文件中的数据
        String[] $data = null ;

        String logName = desc.getCanonicalPath() + ".log";

        File fl = new File(logName);

        if (fl.exists()) {
    
    
            BufferedReader reader = new BufferedReader(new FileReader(fl));
            String data = reader.readLine();
            // 拆分 字符串
            $data = data.split(",");
            reader.close();
        }

        final String[] _data = $data ;

        for (int i = 0; i < threadNum; i++) {
    
    
            final int k = i ;
            Thread thread = new Thread(() -> {
    
    
                // 线程具体要做的事情
                RandomAccessFile log = null ;
                try {
    
    
                    RandomAccessFile in = new RandomAccessFile(src, "r");
                    RandomAccessFile out = new RandomAccessFile(desc, "rw");

                    log = new RandomAccessFile(logName, "rw");
                    // 从指定位置读
                    in.seek(_data ==null ?k * part : Integer.parseInt(_data[k]) );
                    out.seek(_data ==null ?k * part : Integer.parseInt(_data[k]) );

                    byte[] bytes = new byte[1024 * 2];
                    int len = -1, plen = 0;

                    while (true) {
    
    
                        len = in.read(bytes);

                        if (len == -1) {
    
    
                            break;
                        }
                        // 如果不等于 -1 , 则 累加求和
                        plen += len;

                        // 将读取的字节数,放入 到 map 中
                        map.put(k,  plen + (_data ==null ?k * part : Integer.parseInt(_data[k])) );

                        // 将读取到的数据、进行写入
                        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 (plen + (_data ==null ? k * part : Integer.parseInt(_data[k])) >= (k+1) * part ) {
    
    
                            break;
                        }
                    }
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }finally {
    
    
                    try {
    
    
                        if (log !=null) log.close();
                    } catch (IOException e) {
    
    
                        e.printStackTrace();
                    }
                }

            });
            thread.start();
            // 把这5个线程保存到集合中
            list.add(thread);
        }
        for(Thread t : list) {
    
    
            t.join(); // 将线程加入,并阻塞主线程
        }
        // 读取完成后、将日志文件删除即可
        new File(logName).delete();
    }

    /**
     * 支持断点续传
     * @src 拷贝的原文件
     * @desc 拷贝的位置
     */
    public static void transportFile(File src, File desc) throws Exception {
    
    
        transportFile(src, desc, 5);
    }

    public static void transportFile(String src, String desc) throws Exception {
    
    
        transportFile(new File(src), new File(desc));
    }
}

猜你喜欢

转载自blog.csdn.net/qq_16992475/article/details/130009930