不學無數——JAVA中NIO再深入

不學無數的程式設計師發表於2019-03-03

JAVA中NIO再深入

在上一章節的JAVA中的I/O和NIO我們學習瞭如何使用NIO,接下來再深入瞭解一下關於NIO的知識。

緩衝器內部的細節

Buffer資料可以高效地訪問及操作這些資料的四個索引組成。這四個索引是

  • mark:標記,就像遊戲中設定了一個存檔一樣,可以呼叫reset()方法進行迴歸到mark標記的地方。
  • position:位置,其實緩衝器實際上就是一個美化過的陣列,從通道中讀取資料就是放到了底層的陣列。所以其實就像索引一樣。所以positon變數跟蹤已經寫了多少資料。
  • limit:界限,即表明還有多少資料需要取出,或者還有多少空間能夠寫入。
  • capacity:容量,表明緩衝器中可以儲存的最大容量。

在緩衝器中每一個讀寫操作都會改變緩衝器的狀態,用於反應所發生的變化。通過記錄和跟蹤這些變化,緩衝器就能夠內部地管理自己的資源。下面是用於設定和復位索引以及查詢其索引值的方法

方法名 解釋
capacity() 返回緩衝器的容量
clear() 清空緩衝器,將position設定為0,limit設定容量。呼叫此方法複寫緩衝器
flip() 將limit設定為position,position設定為0.此方法用於準備從緩衝區讀取已經寫入的資料
limit() 返回limit值
limit(int lim) 設定limit值
mark() 將mark設定為positon
position() 返回position的值
position(int pos) 設定postion的值
remaining() 返回limit-position的值

接下來我們寫個例子模擬這四個索引的變化情況,例如有一個字串BuXueWuShu。我們交換相鄰的字元。

    private static void symmetricScranble(CharBuffer buffer){
        while (buffer.hasRemaining()){
            buffer.mark();
            char c1 = buffer.get();
            char c2 = buffer.get();
            buffer.reset();
            buffer.put(c2).put(c1);
        }
    }

複製程式碼
    public static void main(String[] args) {
        char [] data = "BuXueWuShu".toCharArray();
        ByteBuffer byteBuffer = ByteBuffer.allocate(data.length*2);
        CharBuffer charBuffer = byteBuffer.asCharBuffer();
        charBuffer.put(data);
        System.out.println(charBuffer.rewind());
        symmetricScranble(charBuffer);
        System.out.println(charBuffer.rewind());
        symmetricScranble(charBuffer);
        System.out.println(charBuffer.rewind());
    }

複製程式碼

rewind()方法是將position設為0 ,mark設為-1

在剛進入symmetricScranble ()方法時的各個索引如下圖所示

不學無數——JAVA中NIO再深入

然後第一次呼叫了mark()方法以後就相當於給mark賦值了,相當於在此設定了一個回檔點。此時索引如下所示

不學無數——JAVA中NIO再深入

然後每次呼叫get()方法Position索引都會改變,在第一次呼叫了兩次get()方法以後,各個索引如下

不學無數——JAVA中NIO再深入

然後呼叫了reset()方法另Position=Mark,此時的索引如下

不學無數——JAVA中NIO再深入

然後每次呼叫put()方法也會改變Position索引的值,

不學無數——JAVA中NIO再深入

注意此時前兩個字元已經互換了位置。然後在第二輪while開始再次改變了Mark索引的值,各個索引如下

不學無數——JAVA中NIO再深入

此時我們應該就知道前面我們說的呼叫clear()方法並不會清除緩衝器裡面的資料的原因了,因為只是將其索引變了而已。

記憶體對映檔案

記憶體對映檔案不是Java引入的概念,而是作業系統提供的一種功能,大部分作業系統都支援。

記憶體對映檔案允許我們建立和修改那些因為太大而不能放入記憶體的檔案。有了記憶體對映檔案,我們就可以假定整個檔案都放在記憶體中,而且可以完全將其視為非常大的陣列進行訪問。所以對於檔案的操作就變為了對於記憶體中的位元組陣列的操作,然後對於位元組陣列的操作會對映到檔案中。這種對映可以對映整個檔案,也可以只對映檔案中的一部分。什麼時候位元組陣列中的的操作會對映到檔案上呢?這是由作業系統內部決定的。

記憶體放不下整個檔案也不要緊,作業系統會自動進行處理,將需要的內容讀到記憶體,將修改的內容儲存到硬碟,將不再使用的記憶體釋放。

如何用NIO將檔案對映到記憶體中呢?下面有個小例子表示將檔案的前1024個位元組對映到記憶體中。

FileChannel fileChannel = new FileInputStream("").getChannel();
MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
複製程式碼

建立一個記憶體對映檔案只需要在通道中呼叫map()方法即可,MapMode有以下三個引數

  • READ_ONLY:建立一個只讀的對映檔案
  • READ_WRITE:建立一個既能讀也能寫的對映檔案
  • PRIVATE:建立一個寫時拷貝(copy-on-write)的對映檔案

我們可以簡單的對比一下用記憶體對映檔案對檔案進行讀寫操作和用快取Buffer對檔案進行讀寫操作的速度比較。

  public static void main(String[] args) throws IOException {
        String fileName="/Users/hupengfei/Downloads/a.sql";
        long t1=System.currentTimeMillis();
        FileChannel fileChannel = new RandomAccessFile(fileName,"rw").getChannel();
        IntBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size()).asIntBuffer();
        map.put(0);
        for (int i = 1; i < 50590; i++) {
            map.put(map.get(i-1));
        }
        fileChannel.close();
        long t=System.currentTimeMillis()-t1;
        System.out.println("Mapped Read/Write:"+t);

        long t2=System.currentTimeMillis();
        RandomAccessFile randomAccessFile = new RandomAccessFile(new File(fileName),"rw");
        randomAccessFile.writeInt(1);
        for (int i = 0 ; i<50590;i++){
            randomAccessFile.seek(randomAccessFile.length()-4);
            randomAccessFile.writeInt(randomAccessFile.readInt());
        }
        randomAccessFile.close();
        long t22=System.currentTimeMillis()-t2;
        System.out.println("Stream Read/Write:"+t22);
    }

複製程式碼

發現列印如下

Mapped Read/Write:29
Stream Read/Write:2439
複製程式碼

檔案越大,那麼這個差異會更明顯。

檔案加鎖

在JDK1.4中引入了檔案加鎖的機制,它允許我們同步的訪問某個作為共享資源的檔案。對於同一檔案競爭的兩個執行緒可能是來自於不同的作業系統,也可能是不同的程式,也可能是相同的程式,例如Java中兩個執行緒對於檔案的競爭。檔案鎖對於其他的作業系統的程式是可見的,因為檔案加鎖是直接對映到了本地作業系統的加鎖工具。

下面舉了一個簡單的關於檔案加鎖的例子

    public static void main(String[] args) throws IOException, InterruptedException {
        FileOutputStream fileOutputStream = new FileOutputStream("/Users/hupengfei/Downloads/a.sql");
        FileLock fileLock = fileOutputStream.getChannel().tryLock();
        if (fileLock != null){
            System.out.println("Locked File");
            TimeUnit.MICROSECONDS.sleep(100);
            fileLock.release();
            System.out.println("Released Lock");
        }
        fileLock.close();
    }

複製程式碼

通過對FileChannel呼叫tryLock()或者lock()方法,就可以獲得整個檔案的FileLock

  • tryLock():是非阻塞的,如果不能獲得鎖,那麼他就會直接從方法呼叫中返回
  • lock():是阻塞的,它要阻塞程式直到鎖可以獲得為止

呼叫FileLock.release()可以釋放鎖。

當然也可以通過以下的方式對於檔案的部分進行上鎖

tryLock(long position,long size,boolean shared)
lock(long position,long size,boolean shared)
複製程式碼

對於加鎖的區域是通過positionsize進行限定的,而第三個引數指定是否為共享鎖。無引數的加鎖方法會對整個檔案進行加鎖,甚至檔案變大以後也是如此。其中鎖的型別是獨佔鎖還是共享鎖可以通過FileLock.isShared()進行查詢。

參考文章

相關文章