Tape源码解析

作者: 不二先生的世界 | 来源:发表于2016-06-11 14:05 被阅读115次

    Tape是一个队列集合类库,主要包含内存对象队列,文件对象队列和任务队列,特别是文件对象队列的设计,使用了RandomAccessFile,可以在新增,移除对象时就把数据保存在文件里,使用起来非常方便。

    使用方法

    Tape的使用方法非常简单,和List差不多,包含方法size(),add(T entry),peek(),remove(),看字面意思就明白,我就不具体展开了,至于TaskQueue的使用,可以看下官方的介绍.

    源码分析

    QueueFile(队列文件)

    内存对象队列和任务队列都比较简单,主要是看到了QueueFile(队列文件)的设计受到了一些启发,在这里和可以和大家聊聊。

    数据结构设计
    QueueFile数据结构.png

    看到上图,我们可以清晰的看到,该数据结构包含头部信息(16 bytes)和元素列表(Length - 16 bytes)。头部信息包含文件长度(4 bytes),元素个数(4 bytes),第一个元素位置(4 bytes),最后一个元素位置(4 bytes)。元素列表里面的单个元素包含元素长度(4 bytes)和元素内容。

    源码设计

    里面主要用到的是RandomAccessFile(随机存储文件),用seek( )方法来访问记录,只需要记住位置和大小即可。

    RandomAccessFile file = new RandomAccessFile("file", "rw");  
    // 以下向file文件中写数据
    file.writeInt(20);
    file.seek(0);// 把文件指针位置设置到文件起始处  
      
    // 以下从file文件中读数据,要注意文件指针的位置
    file.readInt();
    

    在QueueFile里面对RandomAccessFile使用主要就是用到上面这几个方法,我们接下来看看QueueFile源码。

    QueueFile初始化

    public QueueFile(File file) throws IOException {
        if (!file.exists()) initialize(file);//如果文件不存在,则初始化
        raf = open(file);//创建RandomAccessFile
        readHeader();//读取头部信息
      }
    
    private static void initialize(File file) throws IOException {
        // Use a temp file so we don't leave a partially-initialized file.
        File tempFile = new File(file.getPath() + ".tmp");//创建文件*.tmp
        RandomAccessFile raf = open(tempFile);//创建RandomAccessFile
        try {
          raf.setLength(INITIAL_LENGTH);//初始化RandomAccessFile文件长度是4096
          raf.seek(0);// 把文件指针位置设置到文件起始处  
          byte[] headerBuffer = new byte[16];//初始化头部信息bytes
          writeInts(headerBuffer, INITIAL_LENGTH, 0, 0, 0);//将文件长度为4096写入headerBuffer
          raf.write(headerBuffer);//将headerBuffer写入RandomAccessFile文件
        } finally {
          raf.close();
        }
    
        // A rename is atomic.
        if (!tempFile.renameTo(file)) throw new IOException("Rename failed!");//将tempFile重命名成file
      }
    
    private void readHeader() throws IOException {
        raf.seek(0);
        raf.readFully(buffer);
        fileLength = readInt(buffer, 0);
        if (fileLength > raf.length()) {
          throw new IOException("File is truncated. Expected length: " + fileLength + ", Actual length: " + raf.length());
        } else if (fileLength == 0) {
          throw new IOException("File is corrupt; length stored in header is 0.");
        }
        elementCount = readInt(buffer, 4);//读取元素个数
        int firstOffset = readInt(buffer, 8);//读取第一个元素的位置
        int lastOffset = readInt(buffer, 12);//读取最后一个元素的位置
        first = readElement(firstOffset);//读取第一个元素
        last = readElement(lastOffset);//读取最后一个元素
      }
    

    至此,QueueFile初始化完成。

    接下来看下add方法。

      public void add(byte[] data) throws IOException {
        add(data, 0, data.length);
      }
    
      public synchronized void add(byte[] data, int offset, int count) throws IOException {
        nonNull(data, "buffer");
        if ((offset | count) < 0 || count > data.length - offset) {
          throw new IndexOutOfBoundsException();
        }
    
        expandIfNecessary(count);//判断新增内容之后文件长度是否够,如果不够则进行扩大
    
        // Insert a new element after the current last element.
        boolean wasEmpty = isEmpty();
        int position = wasEmpty ? HEADER_LENGTH : wrapPosition(last.position + Element.HEADER_LENGTH + last.length);
        Element newLast = new Element(position, count);
    
        // Write length.
        writeInt(buffer, 0, count);//向buffer写入新元素长度
        ringWrite(newLast.position, buffer, 0, Element.HEADER_LENGTH);//向RandomAccessFile写入新元素长度
    
        // Write data.
        ringWrite(newLast.position + Element.HEADER_LENGTH, data, offset, count);//向RandomAccessFile写入新元素内容
    
        // Commit the addition. If wasEmpty, first == last.
        int firstPosition = wasEmpty ? newLast.position : first.position;
        writeHeader(fileLength, elementCount + 1, firstPosition, newLast.position);//写入头部信息
        last = newLast;
        elementCount++;
        if (wasEmpty) first = last; // first element
      }
    

    peek方法,返回第一个元素内容

    public synchronized byte[] peek() throws IOException {
        if (isEmpty()) return null;
        int length = first.length;
        byte[] data = new byte[length];
        ringRead(first.position + Element.HEADER_LENGTH, data, 0, length);
        return data;
      }
    

    remove方法

    public synchronized void remove() throws IOException {
        if (isEmpty()) throw new NoSuchElementException();
        if (elementCount == 1) {//如果元素个数等于1,则清空全部内容
          clear();
        } else {
          // assert elementCount > 1
          int firstTotalLength = Element.HEADER_LENGTH + first.length;
    
          ringErase(first.position, firstTotalLength);//清除第一个元素数据
    
          int newFirstPosition = wrapPosition(first.position + firstTotalLength);
          ringRead(newFirstPosition, buffer, 0, Element.HEADER_LENGTH);
          int length = readInt(buffer, 0);//新元素内容长度
          writeHeader(fileLength, elementCount - 1, newFirstPosition, last.position);//写入更新之后的头部信息
          elementCount--;
          first = new Element(newFirstPosition, length);
        }
      }
    
    private void ringErase(int position, int length) throws IOException {
        while (length > 0) {
          int chunk = min(length, ZEROES.length);
          ringWrite(position, ZEROES, 0, chunk);
          length -= chunk;
          position += chunk;
        }
      }
    

    需要注意的是,在清除第一个元素数据时,只是将第一个元素的数据设置成0。

    总结

    测试发现,RandomAccessFile在处理大数据的时候读写性能很差,主要原因是RandomAccessFile每读/写一个字节就需对磁盘进行一次I/O操作,所以QueueFile不适合大数据的读写。不过可以对RandomAccessFile进行优化,增加缓冲读写机制,看该文

    参考资料

    Tape
    花1K内存实现高效I/O的RandomAccessFile类

    可以随意转发,也欢迎关注我的简书,我会坚持给大家带来分享。

    相关文章

      网友评论

        本文标题:Tape源码解析

        本文链接:https://www.haomeiwen.com/subject/uxxsdttx.html