如何提高java I/O的执行效率,MappedByteBuffer

如何提高java I/O的执行效率,MappedByteBuffer
复制内容到剪贴板代码:
import java.nio.*;
import java.nio.channel.*;
import java.io.*;
public static void copy(File source, File dest) throws IOException {
FileChannel in = null, out = null;
try {
in = new FileInputStream(source).getChannel();
out = new FileOutputStream(dest).getChannel();

long size = in.size();
MappedByteBuffer buf = in.map(FileChannel.MapMode.READ_ONLY, 0, size);

out.write(buf);
if (in != null) in.close();
if (out != null) out.close();
}
}谈谈MappedByteBuffer

JDK1.4中加入了一个新的包:NIO(java.nio.*).这个库最大的功能就是增加了对异步套接字的支持.
其实在其他语言中,包括在最原始的SOCKET实现(BSD SOCKET),这是一个早有的功能:异步回调读/写事件,通过选择器动态选择感兴趣的事件,等等.不过好在SUN终于也开始支持它了.我想这也是开放的好处之一吧(NIO是作为JSR-51项目引入的).

这里简单讲一下操作流程:

通过把一个套接字通道(SocketChannel)注册到一个选择器(Selector)中,不时调用后者的选择(select)方法就能返回满足的选择键(SelectionKey),键中包含了SOCKET事件信息.


异步套接字对服务器程序来说更具吸引力.一般同步SOCKET服务器的实现都是采用线程池来处理客户请求的,基于请求超时时间和并发线程数目的限制,如果并发处理能力能够达到上千就已经是不错了.异步服务器的能力则至少是它的数倍(有人测试一个简单的ECHO服务程序,说可以达到上万个并发,不知道是否真的能达到).

SocketChannel的读写是通过一个类叫ByteBuffer(java.nio.ByteBuffer)来操作的.这个类本身的设计是不错的,比直接操作byte[]方便多了.

ByteBuffer有两种模式:直接/间接.间接模式最典型(也只有这么一种)的就是HeapByteBuffer,即操作堆内存(byte[]).但是内存毕竟有限,如果我要发送一个1G的文件怎么办?不可能真的去分配1G的内存.这时就必须使用"直接"模式,即MappedByteBuffer,文件映射.

先中断一下,谈谈操作系统的内存管理.一般操作系统的内存分两部分:物理内存;虚拟内存.虚拟内存一般使用的是页面映像文件,即硬盘中的某个(某些)特殊的文件.操作系统负责页面文件内容的读写,这个过程叫"页面中断/切换".

MappedByteBuffer也是类似的,你可以把整个文件(不管文件有多大)看成是一个ByteBuffer.这是一个很好的设计,除了一点,令人头疼的一点.


MappedByteBuffer只能通过调用FileChannel的map()取得,再没有其他方式.但是令人奇怪的是,SUN提供了map()却没有提供unmap().这样会导致什么后果呢?

举个例子,文件test.tmp是一个临时构建的文件,在业务处理(通过SocketChannel发送)完之后将不再有效.一般的做法都是这样的:

(1)File file = new File("test.tmp");
FileInputStream in = new FileInputStream(file);
FileChannel ch = in.getChannel();
MappedByteBuffer buf = ch.map(FileChannel.MapMode.READ_ONLY, 0, file.length());


(2)SocketChannel sch = 已经构造好了;
while (buf.hasRemaining())
sch.write(buf);

(3)ch.close();
in.close();
file.delete();

上面的操作都会正常的完成,除了最后一步:文件无法删除!即使你通过资源管理器直接强制删除也不行,说"文件正在使用".


为什么会出现这种情况?
说"文件正在使用",说明文件句柄没有清零,还有在使用它的地方---就是被MappedByteBuffer占用了!尽管FileChannel,FileInputStream都已经关闭了,但是在map里还打开着一个文件句柄.但是在外部看不见也无法操作它.那么这个句柄在什么时候才会正常地关闭呢?根据JAVADOC的说明,是在垃圾收集的时候.而众所周知垃圾收集是程序根本无法控制的.


既然MappedByteBuffer是从FileChannel中map()出来的,为什么它又不提供unmap()呢?SUN自己也没有讲清楚为什么.O'Reilly的<<Java NIO>>中说是因为"安全"的原因,但是到底unmap()会怎么不安全,作者也没有讲清楚.


在SUN的BUG库中,这个问题在02年就有人提交了BUG报告,但是SUN自己不认为是BUG,而只是一个RFE(Request For Enhancement),有待改进.
好在网上牛人多.在BUG报告(http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4724038)中,有网友提出了一个解决的办法(具体参看上面的URL),可行,至少我在WINDOWS2000下测试是可以的.唯一的不足是并不是每次都能马上生效(文件彻底被删除),有的时候要延迟一会再试.


再抱怨两句.对于网友们的BUG报告,SUN似乎不怎么重视.粗看一下上面的BUG报告,会发现居然上世纪90年代的报告还赫然在列.有兴趣的朋友不妨仔细研究研究.


还有一点忘了说了.ByteBuffer是无法派生的.因为这个抽象类中定义了几个包抽象方法,即实现类只能位于java.nio包中.本来自己实现MappedByteBuffer也不难,只是效率比SUN实现的肯定要低一些.毕竟后者是可以直接与操作系统打交道的.而要是自己实现的化,只能通过一个中间的堆缓冲区进行过渡.


我不知道为什么SUN不提供ByteBuffer的派生.毕竟这是一个很实用的类,如果允许派生,那么我就可以操作的就不仅仅限于堆内存和文件了,我可以扩展到任何存储设备.复制内容到剪贴板代码:

public boolean copyTo(String strSourceFileName, String strDestDir) {
File fileSource = new File(strSourceFileName);
File fileDest = new File(strDestDir);


// 如果源文件不存或源文件是文件夹
if (!fileSource.exists() || !fileSource.isFile()) {
System.out.println("错误: FileOperator.java copyTo函数,\n原因: 源文件["
+ strSourceFileName + "],不存在或是文件夹!");
return false;
}


// 如果目标文件夹不存在
if (!fileDest.isDirectory() || !fileDest.exists()) {
if (!fileDest.mkdirs()) {
System.out.println("错误: FileOperator.java copyTo函数,\n原因:目录文件夹不存,在创建目标文件夹时失败!");
return false;
}
}

try {
String strAbsFilename = strDestDir + File.separator + fileSource.getName();

FileInputStream fileInput = new FileInputStream(strSourceFileName);
FileOutputStream fileOutput = new FileOutputStream(strAbsFilename);

int i = 0;
int count = -1;

long nWriteSize = 0;
long nFileSize = fileSource.length();

byte[] data = new byte[BUFFER];

while (-1 != (count = fileInput.read(data, 0, BUFFER))) {
fileOutput.write(data, 0, count);
nWriteSize += count;
long size = (nWriteSize * 100) / nFileSize;
long t = nWriteSize;
String msg = null;
if (size <= 100 && size >= 0) {
msg = "\r拷贝文件进度: " + size + "% \t" + "\t 已拷贝: " + t;
} else if (size > 100) {
msg = "\r拷贝文件进度: " + 100 + "% \t" + "\t 已拷贝: " + t;
}
}

fileInput.close();
fileOutput.close();

System.out.println("\n拷贝文件成功!");
return true;

} catch (Exception e) {
System.out.println("异常信息:[");
e.printStackTrace();
return false;
}
}
http://www.loohost.com/redirect.php?tid=1967&goto=lastpost
阅读更多
上一篇jar文件使用详解
下一篇Java与随机数
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭