極大提高java I/O效率的方法:談談MappedByteBuffer

  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.     }

發佈了137 篇原創文章 · 獲贊 6 · 訪問量 64萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章