深入分析:用1K内存实现高效I/O的RandomAccessFile类的详解
内存 实现 详解 高效 深入分析 RandomAccessFile
2023-06-13 09:14:54 时间
开发人员迫切需要提高效率,下面分析RandomAccessFile等文件类的源代码,找出其中的症结所在,并加以改进优化,创建一个"性/价比"俱佳的随机文件访问类BufferedRandomAccessFile。
在改进之前先做一个基本测试:逐字节COPY一个12兆的文件(这里牵涉到读和写)。
我们可以看到两者差距约32倍,RandomAccessFile也太慢了。先看看两者关键部分的源代码,对比分析,找出原因。
1.1.[RandomAccessFile]
publicclassRandomAccessFileimplementsDataOutput,DataInput{
publicfinalbytereadByte()throwsIOException{
intch=this.read();
if(ch<0)
thrownewEOFException();
return(byte)(ch);
}
publicnativeintread()throwsIOException;
publicfinalvoidwriteByte(intv)throwsIOException{
write(v);
}
publicnativevoidwrite(intb)throwsIOException;
}
可见,RandomAccessFile每读/写一个字节就需对磁盘进行一次I/O操作。
1.2.[BufferedInputStream]
publicclassBufferedInputStreamextendsFilterInputStream{
privatestaticintdefaultBufferSize=2048;
protectedbytebuf[];//建立读缓存区
publicBufferedInputStream(InputStreamin,intsize){
super(in);
if(size<=0){
thrownewIllegalArgumentException("Buffersize<=0");
}
buf=newbyte[size];
}
publicsynchronizedintread()throwsIOException{
ensureOpen();
if(pos>=count){
fill();
if(pos>=count)
return-1;
}
returnbuf[pos++]&0xff;//直接从BUF[]中读取
}
privatevoidfill()throwsIOException{
if(markpos<0)
pos=0; /*nomark:throwawaythebuffer*/
elseif(pos>=buf.length) /*noroomleftinbuffer*/
if(markpos>0){ /*canthrowawayearlypartofthebuffer*/
intsz=pos-markpos;
System.arraycopy(buf,markpos,buf,0,sz);
pos=sz;
markpos=0;
}elseif(buf.length>=marklimit){
markpos=-1; /*buffergottoobig,invalidatemark*/
pos=0; /*dropbuffercontents*/
}else{ /*growbuffer*/
intnsz=pos*2;
if(nsz>marklimit)
nsz=marklimit;
bytenbuf[]=newbyte[nsz];
System.arraycopy(buf,0,nbuf,0,pos);
buf=nbuf;
}
count=pos;
intn=in.read(buf,pos,buf.length-pos);
if(n>0)
count=n+pos;
}
}
1.3.[BufferedOutputStream]
publicclassBufferedOutputStreamextendsFilterOutputStream{
protectedbytebuf[];//建立写缓存区
publicBufferedOutputStream(OutputStreamout,intsize){
super(out);
if(size<=0){
thrownewIllegalArgumentException("Buffersize<=0");
}
buf=newbyte[size];
}
publicsynchronizedvoidwrite(intb)throwsIOException{
if(count>=buf.length){
flushBuffer();
}
buf[count++]=(byte)b;//直接从BUF[]中读取
}
privatevoidflushBuffer()throwsIOException{
if(count>0){
out.write(buf,0,count);
count=0;
}
}
}
可见,BufferedI/OputStream每读/写一个字节,若要操作的数据在BUF中,就直接对内存的buf[]进行读/写操作;否则从磁盘相应位置填充buf[],再直接对内存的buf[]进行读/写操作,绝大部分的读/写操作是对内存buf[]的操作。
根据1.3得出的结论,现试着对RandomAccessFile类也加上缓冲读写机制。
随机访问类与顺序类不同,前者是通过实现DataInput/DataOutput接口创建的,而后者是扩展FilterInputStream/FilterOutputStream创建的,不能直接照搬。
2.1.开辟缓冲区BUF[默认:1024字节],用作读/写的共用缓冲区。
2.2.先实现读缓冲。
B查BUF中是否存在?若有,直接从BUF中读取,并返回该字符BYTE。
C若没有,则BUF重新定位到该POS所在的位置并把该位置附近的BUFSIZE的字节的文件内容填充BUFFER,返回B。
以下给出关键部分代码及其说明:
publicclassBufferedRandomAccessFileextendsRandomAccessFile{
// byteread(longpos):读取当前文件POS位置所在的字节
// bufstartpos、bufendpos代表BUF映射在当前文件的首/尾偏移地址。
// curpos指当前类文件指针的偏移地址。
publicbyteread(longpos)throwsIOException{
if(pos<this.bufstartpos||pos>this.bufendpos){
this.flushbuf();
this.seek(pos);
if((pos<this.bufstartpos)||(pos>this.bufendpos))
thrownewIOException();
}
this.curpos=pos;
returnthis.buf[(int)(pos-this.bufstartpos)];
}
//voidflushbuf():bufdirty为真,把buf[]中尚未写入磁盘的数据,写入磁盘。
privatevoidflushbuf()throwsIOException{
if(this.bufdirty==true){
if(super.getFilePointer()!=this.bufstartpos){
super.seek(this.bufstartpos);
}
super.write(this.buf,0,this.bufusedsize);
this.bufdirty=false;
}
}
//voidseek(longpos):移动文件指针到pos位置,并把buf[]映射填充至POS
所在的文件块。
publicvoidseek(longpos)throwsIOException{
if((pos<this.bufstartpos)||(pos>this.bufendpos)){//seekposnotinbuf
this.flushbuf();
if((pos>=0)&&(pos<=this.fileendpos)&&(this.fileendpos!=0))
{ //seekposinfile(filelength>0)
this.bufstartpos= pos*bufbitlen/bufbitlen;
this.bufusedsize=this.fillbuf();
}elseif(((pos==0)&&(this.fileendpos==0))
||(pos==this.fileendpos+1))
{ //seekposisappendpos
this.bufstartpos=pos;
this.bufusedsize=0;
}
this.bufendpos=this.bufstartpos+this.bufsize-1;
}
this.curpos=pos;
}
//intfillbuf():根据bufstartpos,填充buf[]。
privateintfillbuf()throwsIOException{
super.seek(this.bufstartpos);
this.bufdirty=false;
returnsuper.read(this.buf);
}
}
至此缓冲读基本实现,逐字节COPY一个12兆的文件(这里牵涉到读和写,用BufferedRandomAccessFile试一下读的速度):
可见速度显著提高,与BufferedInputStream+DataInputStream不相上下。
2.3.实现写缓冲。
B查BUF中是否有该映射?若有,直接向BUF中写入,并返回true。
C若没有,则BUF重新定位到该POS所在的位置,并把该位置附近的BUFSIZE字节的文件内容填充BUFFER,返回B。
下面给出关键部分代码及其说明:
//booleanwrite(bytebw,longpos):向当前文件POS位置写入字节BW。
//根据POS的不同及BUF的位置:存在修改、追加、BUF中、BUF外等情
况。在逻辑判断时,把最可能出现的情况,最先判断,这样可提高速度。
//fileendpos:指示当前文件的尾偏移地址,主要考虑到追加因素
publicbooleanwrite(bytebw,longpos)throwsIOException{
if((pos>=this.bufstartpos)&&(pos<=this.bufendpos)){
//writeposinbuf
this.buf[(int)(pos-this.bufstartpos)]=bw;
this.bufdirty=true;
if(pos==this.fileendpos+1){//writeposisappendpos
this.fileendpos++;
this.bufusedsize++;
}
}else{//writeposnotinbuf
this.seek(pos);
if((pos>=0)&&(pos<=this.fileendpos)&&(this.fileendpos!=0))
{//writeposismodifyfile
this.buf[(int)(pos-this.bufstartpos)]=bw;
}elseif(((pos==0)&&(this.fileendpos==0))
||(pos==this.fileendpos+1)){//writeposisappendpos
this.buf[0]=bw;
this.fileendpos++;
this.bufusedsize=1;
}else{
thrownewIndexOutOfBoundsException();
}
this.bufdirty=true;
}
this.curpos=pos;
returntrue;
}
至此缓冲写基本实现,逐字节COPY一个12兆的文件,(这里牵涉到读和写,结合缓冲读,用BufferedRandomAccessFile试一下读/写的速度):
可见综合读/写速度已超越BufferedInput/OutputStream+DataInput/OutputStream。
优化BufferedRandomAccessFile。
优化原则:
•多重嵌套逻辑判断时,最可能出现的判断,应放在最外层。
•减少不必要的NEW。
这里举一典型的例子:
publicvoidseek(longpos)throwsIOException{
...
this.bufstartpos= pos*bufbitlen/bufbitlen;
//bufbitlen指buf[]的位长,例:若bufsize=1024,则bufbitlen=10。
...
}
seek函数使用在各函数中,调用非常频繁,上面加重的这行语句根据pos和bufsize确定buf[]对应当前文件的映射位置,用"*"、"/"确定,显然不是一个好方法。
优化二:this.bufstartpos=pos&bufmask;//this.bufmask=~((long)this.bufsize-1);
两者效率都比原来好,但后者显然更好,因为前者需要两次移位运算、后者只需一次逻辑与运算(bufmask可以预先得出)。
至此优化基本实现,逐字节COPY一个12兆的文件,(这里牵涉到读和写,结合缓冲读,用优化后BufferedRandomAccessFile试一下读/写的速度):
可见优化尽管不明显,还是比未优化前快了一些,也许这种效果在老式机上会更明显。
以上比较的是顺序存取,即使是随机存取,在绝大多数情况下也不止一个BYTE,所以缓冲机制依然有效。而一般的顺序存取类要实现随机存取就不怎么容易了。
需要完善的地方
提供文件追加功能:
publicbooleanappend(bytebw)throwsIOException{
returnthis.write(bw,this.fileendpos+1);
}
提供文件当前位置修改功能:
publicbooleanwrite(bytebw)throwsIOException{
returnthis.write(bw,this.curpos);
}
返回文件长度(由于BUF读写的原因,与原来的RandomAccessFile类有所不同):
publiclonglength()throwsIOException{
returnthis.max(this.fileendpos+1,this.initfilelen);
}
返回文件当前指针(由于是通过BUF读写的原因,与原来的RandomAccessFile类有所不同):
publiclonggetFilePointer()throwsIOException{
returnthis.curpos;
}
提供对当前位置的多个字节的缓冲写功能:
publicvoidwrite(byteb[],intoff,intlen)throwsIOException{
longwriteendpos=this.curpos+len-1;
if(writeendpos<=this.bufendpos){//b[]incurbuf
System.arraycopy(b,off,this.buf,(int)(this.curpos-this.bufstartpos),
len);
this.bufdirty=true;
this.bufusedsize=(int)(writeendpos-this.bufstartpos+1);
}else{//b[]notincurbuf
super.seek(this.curpos);
super.write(b,off,len);
}
if(writeendpos>this.fileendpos)
this.fileendpos=writeendpos;
this.seek(writeendpos+1);
}
publicvoidwrite(byteb[])throwsIOException{
this.write(b,0,b.length);
}
提供对当前位置的多个字节的缓冲读功能:
publicintread(byteb[],intoff,intlen)throwsIOException{
longreadendpos=this.curpos+len-1;
if(readendpos<=this.bufendpos&&readendpos<=this.fileendpos){
//readinbuf
System.arraycopy(this.buf,(int)(this.curpos-this.bufstartpos),
b,off,len);
}else{//readb[]size>buf[]
if(readendpos>this.fileendpos){//readb[]partinfile
len=(int)(this.length()-this.curpos+1);
}
super.seek(this.curpos);
len=super.read(b,off,len);
readendpos=this.curpos+len-1;
}
this.seek(readendpos+1);
returnlen;
}
publicintread(byteb[])throwsIOException{
returnthis.read(b,0,b.length);
}
publicvoidsetLength(longnewLength)throwsIOException{
if(newLength>0){
this.fileendpos=newLength-1;
}else{
this.fileendpos=0;
}
super.setLength(newLength);
}
publicvoidclose()throwsIOException{
this.flushbuf();
super.close();
}
至此完善工作基本完成,试一下新增的多字节读/写功能,通过同时读/写1024个字节,来COPY一个12兆的文件,(这里牵涉到读和写,用完善后BufferedRandomAccessFile试一下读/写的速度):
与JDK1.4新类MappedByteBuffer+RandomAccessFile的对比?
JDK1.4提供了NIO类,其中MappedByteBuffer类用于映射缓冲,也可以映射随机文件访问,可见JAVA设计者也看到了RandomAccessFile的问题,并加以改进。怎么通过MappedByteBuffer+RandomAccessFile拷贝文件呢?下面就是测试程序的主要部分:
RandomAccessFilerafi=newRandomAccessFile(SrcFile,"r");
RandomAccessFilerafo=newRandomAccessFile(DesFile,"rw");
FileChannelfci=rafi.getChannel();
FileChannelfco=rafo.getChannel();
longsize=fci.size();
MappedByteBuffermbbi=fci.map(FileChannel.MapMode.READ_ONLY,0,size);
MappedByteBuffermbbo=fco.map(FileChannel.MapMode.READ_WRITE,0,size);
longstart=System.currentTimeMillis();
for(inti=0;i<size;i++){
byteb=mbbi.get(i);
mbbo.put(i,b);
}
fcin.close();
fcout.close();
rafi.close();
rafo.close();
System.out.println("Spend:"+(double)(System.currentTimeMillis()-start)/1000+"s");
试一下JDK1.4的映射缓冲读/写功能,逐字节COPY一个12兆的文件,(这里牵涉到读和写):
确实不错,看来JDK1.4比1.3有了极大的进步。如果以后采用1.4版本开发软件时,需要对文件进行随机访问,建议采用MappedByteBuffer+RandomAccessFile的方式。但鉴于目前采用JDK1.3及以前的版本开发的程序占绝大多数的实际情况,如果您开发的JAVA程序使用了RandomAccessFile类来随机访问文件,并因其性能不佳,而担心遭用户诟病,请试用本文所提供的BufferedRandomAccessFile类,不必推翻重写,只需IMPORT本类,把所有的RandomAccessFile改为BufferedRandomAccessFile,您的程序的性能将得到极大的提升,您所要做的就这么简单。
未来的考虑
读者可在此基础上建立多页缓存及缓存淘汰机制,以应付对随机访问强度大的应用。
相关文章
- Java内部类有坑,100%内存泄露!
- 细说|Linux内存泄漏检测实现原理与实现
- Linux源码学习笔记day4 操作系统怎么把自己弄到内存里的?
- GPDB如何使用valgrind进行内存检测
- 【Linux 内核 内存管理】物理内存组织结构 ② ( 内存模型 | 平坦内存 | 稀疏内存 | 非连续内存 | 内存管理系统三级结构 | 节点 Node | 区域 Zone | 页 Page )
- 设置Redis最大占用内存的实现
- java 读取文件——按照行取出(使用BufferedReader和一次将数据保存到内存两种实现方式)详解编程语言
- Java常见的几种内存溢出及解决方案详解编程语言
- Oracle优化:实现内存最大化(oracle设置内存)
- 限制Linux内存使用:实现有效运维(linux限制内存)
- Linux支持高达256TB内存(linux支持多大内存)
- 快速瞬间:Redis内存数据库技术(redis内存数据库)
- 实现快速检索:SQL Server内存表(sqlserver内存表)
- Redis:实现高性能内存数据库(redis内存数据库)
- Linux mtrace: 实现系统内存跟踪的利器(linuxmtrace)
- SQL Server扩内存:提升系统性能的新方法(sqlserver扩内存)
- MSSQL内存管理:优化数据库性能的关键(mssql内存)
- Redis突破内存瓶颈,实现性能压缩(redis内存压缩)
- 析构Oracle的内存分配策略(oracle内存分配策略)
- Redis集群优化清理内存实现稳定运行(redis集群清理内存)
- 最高性能利用Redis实现内存池的管理(内存池redis)
- Redis集群优化内存使用方式实现增长(redis集群内存增长)
- Android图片占用内存全面分析