1. 程式人生 > >NIO學習筆記一之Buffer

NIO學習筆記一之Buffer

參考:http://ifeve.com/buffers/

有一些個人理解,大家辯證地看,有問題的地方,還請大家指出。

Java NIO中的Buffer用於和NIO通道進行互動。如你所知,資料是從通道讀入緩衝區,從緩衝區寫入到通道中的。

緩衝區本質上是一塊可以寫入資料,然後可以從中讀取資料的記憶體。這塊記憶體被包裝成NIO Buffer物件,並提供了一組方法,用來方便的訪問該塊記憶體。

1.Buffer的基本用法

使用Buffer讀寫資料一般遵循以下幾個步驟:

  • 向Buffer寫資料
  • 呼叫filp()方法,該方法的作用是將Buffer從寫模式切換到讀模式,後面會具體介紹
  • 從Buffer中讀取資料
  • 呼叫clear()方法或是compact()方法,這兩個方法的作用主要是清空緩衝區,其實並不是真的清空緩衝區的資料,只是更改了幾個標誌位,後面會具體介紹

一個栗子:

    ByteBuffer buffer = ByteBuffer.allocate(48);
    buffer.put((byte)4);
    buffer.flip();
    for(int i=0;i<buffer.limit();i++){
        System.out.println(buffer.get());
    }

Buffer的幾個重要成員變數

    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

capacity

字面意思理解,這個變數指的是容量,我們可以把它理解為Buffer的容量,也就是我們最多可以給Buffer寫入capacity大小的資料

position:

position表示當前讀取(或是寫入)的位置,寫入(或是讀取)一個數據後,position會指向下一個可讀(或是可寫)的位置。

limit:

limit表示我們最多可以讀到(或寫到)limit位置,這個變數容易和capacity混淆。

這個變量出現的原因主要是我們讀和寫都用的是一塊記憶體,所以需要一個標記,來只是我們最多可以讀到(或寫到)什麼位置。

mark:

這個變數是一個記號,用來記住當前的position,之後介紹mark()方法的時候會具體介紹。

3.Buffer的型別

Buffer主要有以下一種型別,我們主要會以ByteBuffer為主介紹。

4.Buffer的分配

堆中分配:

    ByteBuffer buffer = ByteBuffer.allocate(48);

我們來看一下allocate(int capacity)的程式碼:

    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }

我們可以看到,首先做了基本的校驗,然後就return了一個HeapByteBuffer的物件。

我們繼續看一下HeapByteBuffer的構造方法:

    HeapByteBuffer(int cap, int lim) {            // package-private
        super(-1, 0, lim, cap, new byte[cap], 0);
    }

我們可以看到,該構造方法中直接呼叫了父類(也就是ByteBuffer類)的構造方法:

    ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                 byte[] hb, int offset)
    {
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }

我們可以看到:這兩個方法都是包訪問許可權,也就是隻有java.nio包中的類可以呼叫(直白地說,就是我們是不能直接呼叫的),所以這個allocate(int capacity)是一個工廠方法!

ByteBuffer中呼叫的是HeapByteBuffer的構造方法,也是就在JVM堆中分配記憶體,所以我們呼叫allocate(int capacity)得到的是普通的物件。

直接記憶體中分配:

我們還可以在直接記憶體中分配ByteBuffer物件:

    ByteBuffer buffer = ByteBuffer.allocateDirect(48);

我們來看一下是怎麼分配的:

    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }

再點進去看一下DirectByteBuffer(int cap):

    DirectByteBuffer(int cap) {                   // package-private

        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

我們可以看到它是通過呼叫unsafe的allocateMemory(long size)來分配記憶體的,我們由unsafe這個名字就可以知道,它是危險的,它使JVM 和作業系統核心可以交流。

unsafe的allocateMemory(long size)是一個本地方法,我們暫時看不到了。

但是,我們可以知道,allocateMemory(long size)分配的是直接記憶體,也就是JVM堆外記憶體,而allocate(int capacity)分配的是堆內記憶體。

為什麼要支援分配直接記憶體呢?這個問題從網路分層來說起吧:

我們的TCP/IP模型,將網路分為了幾大層:應用層、傳輸層、網路層和鏈路層。其中,傳輸層(包括傳輸層)以下是執行在核心空間的,應用層是執行在使用者空間的。我們要用通過網路接收資料,要將核心空間的資料複製要使用者空間;同樣地,要通過網路傳送資料,要將使用者空間的資料複製到核心空間,再進行傳送。

這樣複製來複制去效率未免很低,於是我們可以通過直接分配直接記憶體的方式,來減少一次複製。

但是這個直接記憶體用起來是有一些注意事項的,後面我會專門寫一篇文章來總結這個。

4.幾個方法介紹

flip()方法

flip方法將Buffer從寫模式切換到讀模式。呼叫flip()方法會將position設回0,並將limit設定成之前position的值。

    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

position為寫的資料的位置,表示就寫了這麼多的資料,我們讀當然不能超過它啦,於是就將limit設定為position,我們讀要從頭開始讀,就將position置為0。

rewind()方法

Buffer.rewind()將position設回0,所以你可以重讀Buffer中的所有資料。limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)。

    public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }

clear()方法

一旦讀完Buffer中的資料,需要讓Buffer準備好再次被寫入。可以通過clear()來完成。

clear()方法將position置為0,limit置為capacity,mark置為-1,也就是將一切置為了原始狀態。我們可以看到,它並沒有真正地清空緩衝區的資料,只是將標誌位還原。

    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

compact()方法

compact()方法將所有未讀的資料拷貝到Buffer起始處。然後將position設到最後一個未讀元素的後面。

mark()方法

這個方法是與下面的reset()方法配套使用的,這個方法很簡單,只是將當前的position記錄在mark上

    public final Buffer mark() {
        mark = position;
        return this;
    }

reset()方法

這個方法就是將position恢復為之間記錄下來的值

    public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();//必要的校驗
        position = m;
        return this;
    }