Tape是一个队列集合类库,主要包含内存对象队列,文件对象队列和任务队列,特别是文件对象队列的设计,使用了RandomAccessFile,可以在新增,移除对象时就把数据保存在文件里,使用起来非常方便。
使用方法
Tape的使用方法非常简单,和List差不多,包含方法size(),add(T entry),peek(),remove(),看字面意思就明白,我就不具体展开了,至于TaskQueue的使用,可以看下官方的介绍.
源码分析
QueueFile(队列文件)
内存对象队列和任务队列都比较简单,主要是看到了QueueFile(队列文件)的设计受到了一些启发,在这里和可以和大家聊聊。
数据结构设计
看到上图,我们可以清晰的看到,该数据结构包含头部信息(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类
可以随意转发,也欢迎关注我的简书,我会坚持给大家带来分享。