现在的位置: 首页 > 综合 > 正文

极大提高java I/O效率的方法:谈谈MappedByteBuffer

2013年01月08日 ⁄ 综合 ⁄ 共 4741字 ⁄ 字号 评论关闭
  1. import java.nio.*;
  2. import java.nio.channel.*;
  3. import java.io.*;
  4. public static void copy(File source, File dest) throws IOException {
  5.  FileChannel in = null, out = null;
  6.  try { 
  7.   in = new FileInputStream(source).getChannel();
  8.   out = new FileOutputStream(dest).getChannel();
  9.  
  10.   long size = in.size();
  11.   MappedByteBuffer buf = in.map(FileChannel.MapMode.READ_ONLY, 0, size);
  12.  
  13.   out.write(buf);
  14.   if (in != null) in.close();
  15.   if (out != null) out.close();
  16.  }
  17. }

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的派生.毕竟这是一个很实用的类,如果允许派生,那么我就可以操作的就不仅仅限于堆内存和文件了,我可以扩展到任何存储设备.

  1. public boolean copyTo(String strSourceFileName, String strDestDir) {
  2. File fileSource = new File(strSourceFileName);
  3. File fileDest = new File(strDestDir);
  4.  
  5. // 如果源文件不存或源文件是文件夹
  6. if (!fileSource.exists() || !fileSource.isFile()) {
  7. System.out.println("错误: FileOperator.java copyTo函数,/n原因: 源文件["
  8. + strSourceFileName + "],不存在或是文件夹!");
  9. return false;
  10. }
  11.  
  12. // 如果目标文件夹不存在
  13. if (!fileDest.isDirectory() || !fileDest.exists()) {
  14. if (!fileDest.mkdirs()) {
  15. System.out.println("错误: FileOperator.java copyTo函数,/n原因:目录文件夹不存,在创建目标文件夹时失败!");
  16. return false;
  17. }
  18. }
  19.  
  20. try {
  21. String strAbsFilename = strDestDir + File.separator + fileSource.getName();
  22.  
  23. FileInputStream fileInput = new FileInputStream(strSourceFileName);
  24. FileOutputStream fileOutput = new FileOutputStream(strAbsFilename);
  25.  
  26. int i = 0;
  27. int count = -1;
  28.  
  29. long nWriteSize = 0;
  30. long nFileSize = fileSource.length();
  31.  
  32. byte[] data = new byte[BUFFER];
  33.  
  34. while (-1 != (count = fileInput.read(data, 0, BUFFER))) {
  35. fileOutput.write(data, 0, count);
  36. nWriteSize += count;
  37. long size = (nWriteSize * 100) / nFileSize;
  38. long t = nWriteSize;
  39. String msg = null;
  40. if (size <= 100 && size >= 0) {
  41. msg = "/r拷贝文件进度: " + size + "% /t" + "/t 已拷贝: " + t;
  42. else if (size > 100) {
  43. msg = "/r拷贝文件进度: " + 100 + "% /t" + "/t 已拷贝: " + t;
  44. }
  45. }
  46.  
  47. fileInput.close();
  48. fileOutput.close();
  49.  
  50. System.out.println("/n拷贝文件成功!");
  51. return true;
  52.  
  53. catch (Exception e) {
  54. System.out.println("异常信息:[");
  55. e.printStackTrace();
  56. return false;
  57. }
  58. }

将那位仁兄的代码贴在以下:

  1. public static void clean(final Object buffer) throws Exception {
  2.         AccessController.doPrivileged(new PrivilegedAction() {
  3.             public Object run() {
  4.                 try {
  5.                     Method getCleanerMethod = buffer.getClass().getMethod("cleaner"new Class[0]);
  6.                     getCleanerMethod.setAccessible(true);
  7.                     sun.misc.Cleaner cleaner = (sun.misc.Cleaner) getCleanerMethod.invoke(buffer, new Object[0]);
  8.                     cleaner.clean();
  9.                 } catch (Exception e) {
  10.                     e.printStackTrace();
  11.                 }
  12.                 return null;
  13.             }
  14.         });
  15.     }

抱歉!评论已关闭.