JAVA NIO之檔案通道
1.簡介
通道是 Java NIO 的核心內容之一,在使用上,通道需和快取類(ByteBuffer)配合完成讀寫等操作。與傳統的流式 IO 中資料單向流動不同,通道中的資料可以雙向流動。通道既可以讀,也可以寫。這裡我們舉個例子說明一下,我們可以把通道看做水管,把快取看做水塔,把檔案看做水庫,把水看做資料。當從磁碟中將檔案資料讀取到快取中時,就是從水庫向水塔裡抽水。當然,從磁盤裡讀取資料並不會將讀取的部分從磁盤裡刪除,但從水庫裡抽水,則水庫裡的水量在無補充的情況下確實變少了。當然,這只是一個小問題,大家不要扣這個細節哈,繼續往下說。當水塔中儲存了水之後,我們可以用這些水燒飯,澆花等,這就相當於處理快取的資料。過了一段時間後,水塔需要進行清洗。這個時候需要把水塔裡的水放回水庫中,這就相當於向磁碟中寫入資料。通過這裡例子,大家應該知道通道是什麼了,以及有什麼用。既然知道了,那麼我們繼續往下看。
Java NIO 出現在 JDK 1.4 中,由於 NIO 效率高於傳統的 IO,所以 Sun 公司從底層對傳統 IO 的實現進行了修改。修改的方式就是在保證相容性的情況下,使用 NIO 重構 IO 的方法實現,無形中提高了傳統 IO 的效率。
2.基本操作
通道型別分為兩種,一種是面向檔案的,另一種是面向網路的。具體的類宣告如下:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
正如上列表,NIO 通道涵蓋了檔案 IO,TCP 和 UDP 網路 IO 等通道型別。本文我們先來說說檔案通道。
2.1 建立通道
FileChannel 是一個用於連線檔案的通道,通過該通道,既可以從檔案中讀取,也可以向檔案中寫入資料。與SocketChannel 不同,FileChannel 無法設定為非阻塞模式,這意味著它只能執行在阻塞模式下。在使用FileChannel 之前,需要先開啟它。由於 FileChannel 是一個抽象類,所以不能通過直接建立而來。必須通過像 InputStream、OutputStream 或 RandomAccessFile 等例項獲取一個 FileChannel 例項。
|
|
2.2 讀寫操作
讀寫操作比較簡單,這裡直接上程式碼了。下面的程式碼會先向檔案中寫入資料,然後再將寫入的資料讀出來並列印。程式碼如下:
|
|
上面的程式碼輸出結果如下:
2.3 資料轉移操作
我們有時需要將一個檔案中的內容複製到另一個檔案中去,最容易想到的做法是利用傳統的 IO 將原始檔中的內容讀取到記憶體中,然後再往目標檔案中寫入。現在,有了 NIO,我們可以利用更方便快捷的方式去完成複製操作。FileChannel 提供了一對資料轉移方法 - transferFrom/transferTo,通過使用這兩個方法,即可簡化檔案複製操作。
|
|
通過上面的程式碼,我們可以明顯感受到,利用 transferTo 減少了編碼量。那麼為什麼利用 transferTo 可以減少編碼量呢?在解答這個問題前,先來說說程式讀取資料和寫入檔案的過程。
我們現在所使用的 PC 作業系統,將記憶體分為了核心空間和使用者空間。作業系統的核心和一些硬體的驅動程式就是執行在核心空間內,而使用者空間就是我們自己寫的程式所能執行的記憶體區域。這裡,當我們呼叫 read 從磁碟中讀取資料時,核心會首先將資料讀取到核心空間中,然後再將資料從核心空間複製到使用者空間內。也就是說,我們需要通過核心進行資料中轉。同樣,寫入資料也是如此。系統先從使用者空間將資料拷貝到核心空間中,然後再由核心空間向磁碟寫入。相關示意圖如下:
與上面的資料流向不同,FileChannel 的 transferTo 方法底層基於 sendfile64(Linux 平臺下)系統呼叫實現。sendfile64 會直接在核心空間內進行資料拷貝,免去了核心往使用者空間拷貝,使用者空間再往核心空間拷貝這兩步操作,因此提高了效率。其示意圖如下:
通過上面的講解,大家應該知道了 transferTo 和 transferFrom 的效率會高於傳統的 read 和 write 在效率上的區別。區別的原因在於免去了核心空間和使用者空間的相互拷貝,雖然記憶體間拷貝的速度比較快,但涉及到大量的資料拷貝時,相互拷貝的帶來的消耗是不應該被忽略的。
講完了背景知識,咱們再來看看 FileChannel 是怎樣呼叫 sendfile64 這個函式的。相關程式碼如下:
|
|
從上面程式碼(transferToDirectly 方法可以在 openjdk/jdk/src/share/classes/sun/nio/ch/FileChannelImpl.java 中找到)中可以看得出 transferTo 的呼叫路徑,先是呼叫 transferToDirectly,然後 transferToDirectly 再呼叫 transferTo0。transferTo0 是 native 型別的方法,我們再去看看 transferTo0 是怎樣實現的,其程式碼在openjdk/jdk/src/solaris/native/sun/nio/ch/FileChannelImpl.c
中。
|
|
如上所示,transferTo0 最終呼叫了 sendfile64 函式,關於 sendfile64 這個系統呼叫的詳細說明,請參考 man-page,這裡就不展開說明了。
2.4 記憶體對映
記憶體對映這個概念源自作業系統,是指將一個檔案對映到某一段虛擬記憶體(實體記憶體可能不連續)上去。我們通過對這段虛擬記憶體的讀寫即可達到對檔案的讀寫的效果,從而可以簡化對檔案的操作。當然,這只是記憶體對映的一個優點。記憶體對映還有其他的一些優點,比如兩個程序對映同一個檔案,可以實現程序間通訊。再比如,C 程式執行時需要 C 標準庫支援,作業系統將 C 標準庫放到了記憶體中,普通的 C 程式只需要將 C 標準庫對映到自己的程序空間內就行了,從而可以降低記憶體佔用。以上簡單介紹了記憶體對映的概念及作用,關於這方面的知識,建議大家去看《深入理解計算機系統》關於記憶體對映的章節,講的很好。
Unix/Linux 作業系統記憶體對映的系統呼叫mmap
,Java 在這個系統呼叫的基礎上,封裝了 Java 的記憶體對映方法。這裡我就不一步一步往下追蹤了,大家有興趣可以自己追蹤一下 Java 封裝的記憶體對映方法的呼叫棧。下面來簡單的示例演示一下記憶體對映的用法:
|
|
上面的程式碼從標準輸入中獲取資料,然後將資料通過記憶體對映快取寫入到檔案中。程式碼執行結果如下:
接下來在用 C 程式碼演示上面程式碼的功能,如下:
|
|
關於 mmap 函式的引數說明,這裡就不細說了,大家可以參考 man-page。上面的程式碼執行結果如下:
關於記憶體對映就說到了,更深入的分析需要涉及到很多作業系統層面的東西。我對這些東西瞭解的也不多,所以就不繼續分析了,慚愧慚愧。
2.5 其他操作
FileChannel 還有一些其他的方法,這裡通過一個表格來列舉這些方法,就不一一展開說明了。如下:
方法名 | 用途 |
---|---|
position | 返回或修改通道讀寫位置 |
size | 獲取通道所關聯檔案的大小 |
truncate | 截斷通道所關聯的檔案 |
force | 強制將通道中的新資料重新整理到檔案中 |
close | 關閉通道 |
lock | 對通道檔案進行加鎖 |
以上所列舉的方法用起來比較簡單,大家自己寫程式碼驗證一下吧,這裡就不貼程式碼了。
3.總結
以上章節對 NIO 檔案通道的用法和部分方法的實現進行了簡單分析。從上面的分析可以看出,NIO FileChannel 在實現上,實際上是對底層作業系統的一些 API 進行了再次封裝,也就是一層皮。有了這層封裝後,對上就遮蔽了底層 API 的細節,以降低使用難度。Java 為了提高開發效率,遮蔽了作業系統層面的細節。雖然 Java 可以遮蔽這些細節,但作為開發人員,我覺得我們不能也去遮蔽這些細節(雖然不瞭解這些細節也能寫程式碼),有時間還是應該多瞭解瞭解這些底層的東西。畢竟要想往更高的層次發展,這些底層的知識必不可少。說到這裡,感覺很慚愧,我的技術基礎也很薄弱。大學期間沒有意識到專業基礎課的重要性,學了很多東西,但忽略了基礎。好在工作不久後看了很多牛人的部落格,也意識到了自己的不足。現在靜下心來打基礎,算是亡羊補牢吧。
好了,關於檔案通道的內容這裡就說到這,謝謝大家的閱讀。
參考
from:http://www.tianxiaobo.com/2018/03/24/JAVA-NIO%E4%B9%8B%E6%96%87%E4%BB%B6%E9%80%9A%E9%81%93/