@author ixenosjava
前提:內存的訪問速度比磁盤高几個數量級,可是基本的IO操做是直接調用native方法得到驅動和磁盤交互的,IO速度限制在磁盤速度上數組
由此,就有了緩存的思想,將磁盤內容預先緩存在內存上,這樣當供大於求的時候IO速度基本就是之內存的訪問速度爲主,例如BufferedInput/OutputStream等緩存
而咱們知道大多數OS均可以利用虛擬內存實現將一個文件或者文件的一部分映射到內存中,而後,這個文件就能夠看成是內存數組同樣地訪問,咱們能夠把它當作一種「永久的緩存」安全
內存映射文件:內存映射文件容許咱們建立和修改那些由於太大而不能放入內存的文件,此時就能夠假定整個文件都放在內存中,並且能夠徹底把它當成很是大的數組來訪問(隨機訪問)網絡
如下是四大文件操做對比:數據結構
本圖使用思惟導圖軟件XMind製做多線程
在Core Java II中進行了這麼一個實驗:在同一臺機器上,對JDK的jre/lib目錄中的37MB的rt.jar文件分別用以上四種操做來計算CRC32校驗和,記錄下了以下時間併發
方法 | 時間 |
普通輸入流 | 110s |
帶緩衝的輸入流 | 9.9s |
隨機訪問文件 | 162s |
內存映射文件 | 7.2s |
這個小實驗也驗證了內存映射文件這個方法的可行性,因爲具備隨機訪問的功能(映射在內存數組),因此經常使用來替代RandomAccessFile。app
固然,對於中等尺寸文件的順序讀入則沒有必要使用內存映射以避免佔用本就有限的I/O資源,這時應當使用帶緩衝的輸入流。dom
java.nio包使得內存映射變得十分簡單
一、首先,從文件中得到一個通道(channel)。通道是用於磁盤文件的一種抽象,它使咱們能夠訪問諸如內存映射、文件加鎖機制(下文緩衝區數據結構部分將提到)、文件間快速數據傳遞等操做系統特性。
1 FileChannel channel = FileChannel.open(path, options);
還能經過在一個打開的 File 對象(RandomAccessFile、FileInputStream 或 FileOutputStream)上調用 getChannel() 方法獲取。調用 getChannel() 方法會返回一個鏈接到相同文件的 FileChannel 對象且該 FileChannel 對象具備與 File 對象相同的訪問權限
二、而後,經過調用FileChannel類的map方法進行內存映射,map方法從這個通道中得到一個MappedByteBuffer對象(ByteBuffer的子類)。
你能夠指定想要映射的文件區域與映射模式,支持的模式有3種:
1 import java.io.*; 2 import java.nio.*; 3 import java.nio.channels.*; 4 import java.nio.file.*; 5 import java.util.zip.*; 6 7 public class MemoryMapTest 8 { 9 10 public static long checksumMappedFile(Path filename) throws IOException 11 { 12 //直接經過傳入的Path打開文件通道 13 try (FileChannel channel = FileChannel.open(filename)) 14 { 15 CRC32 crc = new CRC32(); 16 int length = (int) channel.size(); 17 //經過通道的map方法映射內存 18 MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, length); 19 20 for (int p = 0; p < length; p++) 21 { 22 int c = buffer.get(p); 23 crc.update(c); 24 } 25 return crc.getValue(); 26 } 27 } 28 29 public static void main(String[] args) throws IOException 30 { 31 System.out.println("Mapped File:"); 32 start = System.currentTimeMillis(); 33 crcValue = checksumMappedFile(filename); 34 end = System.currentTimeMillis(); 35 System.out.println(Long.toHexString(crcValue)); 36 System.out.println((end - start) + " milliseconds"); 37 } 38 }
三、一旦有了緩衝區,就可使用ByteBuffer類和Buffer超類的方法來讀寫數據
緩衝區支持順序和隨機數據訪問:
順序:有一個能夠經過get和put操做來移動的位置
1 while(buffer.hasRemaining()){ 2 byte b = buffer.get(); //get當前位置 3 ... 4 }
隨機:能夠按內存數組索引訪問
1 for(int i=0; i<buffer.limit(); i++){ 2 byte b = buffer.get(i); //這個get能指定索引 3 ... 4 }
能夠用下面的方法來讀寫數據到一個字節數組(destination array):
get(byte[] bytes) /get(byte[] bytes, int offset, int length)
The method transfers bytes from this buffer into the given destination array.
還有下列getXxx方法:getInt, getLong, getShort, getChar, getFloat, getDouble 用來讀入在文件中存儲爲二進制值的基本類型值
關於二進制數據排序機制不一樣的讀取問題:
咱們知道,Java對二進制數據使用高位在前的排序機制(好比 0XA就是 0000 1010,高位在前低位在後),
可是,若是須要低位在前的排序方式(0101 0000)處理二進制數字的文件,需調用:
buffer.order(ByteOrder.LITTLE_ENDIAN);
要查詢緩衝區內當前的字節順序,能夠調用:
ByteOrder b = buffer.order();
要向緩衝區寫數字,使用對應的putXxx方法,在恰當的時機,以及當通道關閉時,會將這些修改寫回到文件中的哦。
在使用內存映射時,咱們既能夠建立單一的緩衝區橫跨整個文件或者感興趣的文件區域,也可使用更多的緩衝區來讀寫大小適度的信息塊。
這一小節,就來說講緩衝區Buffer對象上的基本操做。
緩衝區是具備相同基本類型的數值構成的數組(數組在內存中建立),Buffer類是一個抽象類,有如下具體的子類:ByteBuffer,CharBuffer,DoubleBuffer,IntBuffer,LongBuffer和ShortBuffer。(注意StringBuffer跟這些人不要緊,並且String本質是引用類型)
實踐中,最經常使用的是ByteBuffer和CharBuffer。
每一個緩衝區都具備:
一、一個恆定的容量;
二、一個讀寫位置,下一個值將在此進行讀寫;
三、一個界限,超過他沒法讀寫;
四、一個可選的標記,用於重複一個讀入或寫出操做;
0≤標記≤位置≤界限≤容量
一、寫:一開始時位置爲0,界限等於容量,當咱們不斷調用put添值到緩衝區中,直至耗盡全部數據或者寫出的數據集量達到容量大小時,就該進行讀入操做了;
二、讀:這時調用 flip 方法將界限設置到當前位置(至關於trim),並把位置復位到0(爲了讀操做),如今在remaining方法返回(界限 — 位置)正數時,不斷調用get;
三、復位:將緩衝區中全部值讀入後,調用clear(位置復位到0,界限復位到容量)使緩衝區爲下一次寫循環作準備;
四、復讀:想復讀緩衝區,可調用rewind或mark/reset方法;
緩衝區的得到:
A、內存映射時使用的是MappedByteBuffer,這是ByteBuffer的子類,由FileChannel的map()方法調用
B、餓漢要獲取緩衝區,可調用ByteBuffer.allocate或ByteBuffer.wrap這樣的靜態方法,而後用來自某個通道的數據填充緩衝區,或者將緩衝區的內容寫出通道中:
1 ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE); 2 3 //填充緩衝區 4 channel1.read(buffer); 5 //將Channel位置指定到newpos,做爲覆蓋文件內容的起點 6 channel1.position(newpos); 7 //將Buffer界限設置到當前位置,準備寫出,注意區別Buffer和Channel的position,二者是不一樣的概念 8 buffer.flip(); 9 //將緩衝區數據寫出通道中 10 channel.write(buffer);
這些方法和RandomAccessFile類的方法相似,但性能更高,所以經常使用以代替隨機訪問文件。
線程安全:咱們知道多線程併發修改共享數據會產生安全問題——競爭條件,爲了保證對數據的原子性操做——同步存取,咱們有了synchronized關鍵字添加隱式鎖以及ReentranLock添加顯式鎖。可是多進程的同步存取又該怎麼實現呢?
進程安全:OS有個文件加鎖機制,因爲通道是對磁盤的一種抽象,FileChannel所以也實現了文件鎖,能夠調用其lock或tryLock方法進行鎖定。
文件鎖示例:鎖定一個文件
1 FileChannel channel = FileChannel.open(path); 2 3 //調用lock,阻塞 4 FileLock lock = channel.lock(); 5 6 //調用tryLock,當即響應 7 FileLock lock = channel.tryLock();
一、第一個調用 lock() 會阻塞直至可得到鎖,而第二個調用 tryLock() 將當即返回,要麼得到鎖,要麼在鎖不可得到的狀況家返回null;
二、這個文件將保持鎖定狀態,直至這個通道關閉,或者在鎖上調用了release方法;
三、還能夠鎖定文件的一部分:FileLock lock(long start, long size, boolean shared) 或 FileLock tryLock(long start, long size, boolean shared)
a)若是shared標誌位false,則鎖定文件的目的是讀寫,而若是爲true,則這是一個共享鎖,容許多個進程從文件中讀入,並阻止任何進程得到獨佔的鎖。調用FIleLock的isShared可查詢當前持有的文件鎖類型。
b)若是鎖了文件的尾部,但文件長度隨後增加超過了鎖定部分,那麼超過的任然是不鎖定的,此時須要使用 Long.MAX_VALUE 來表示尺寸。
四、要確保在操做完成時釋放鎖,可用 try-with-resources 語句(FileLock實現了AutoCloseable接口)
1 try(FileLock lock = channel.lock()){ 2 ... 3 }
手動釋放鎖可調用FileLock對象的close()方法
注意點:
一、文件加鎖機制是依賴於操做系統的
二、意外的建議鎖:在某些系統中文件鎖僅僅是建議性的,可能出現一個應用未能獲得鎖,它仍舊能夠向被另外一個進程併發鎖定的文件執行寫操做;
三、意外的原子性:在某些系統中,不能在鎖定一個文件的同時將其映射到內存中,原子性;
四、意外的全釋放:在某些系統中,關閉一個通道會釋放由JVM持有的底層文件上的全部鎖,所以避免在同一個鎖定文件上使用多個通道,否則其餘通道的鎖也可能被釋放!
五、不可重入鎖:文件鎖是由整個JVM持有的,兩個由同一VM啓動的程序不可能得到在同一個文件上的鎖,若是嘗試對VM上已加鎖的文件再加鎖,將拋出OverlappingFileLockException;
(注意:多線程的ReentranLock是可重入的!簡稱可重入鎖,而文件鎖是不可重入鎖)
六、在網絡文件系統上鎖定文件是高度依賴於系統的,儘可能避免使用文件鎖。