1. 程式人生 > >NIO 之 ByteBuffer實現原理

NIO 之 ByteBuffer實現原理

前言

Java NIO 主要由下面3部分組成:

  • Buffer
  • Channel
  • Selector

在傳統IO中,流是基於位元組的方式進行讀寫的。 在NIO中,使用通道(Channel)基於緩衝區資料塊的讀寫。

流是基於位元組一個一個的讀取和寫入。 通道是基於塊的方式進行讀取和寫入。

Buffer 類結構圖

Buffer 的類結構圖如下:

Buffer類結構圖

從圖中發現java中8中基本的型別,除了boolean外,其它的都有特定的Buffer子類。

Buffer類分析

Filed

每個緩衝區都有這4個屬性,無論緩衝區是何種型別都有相同的方法來設定這些值

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

1. 標記(mark)

初始值-1,表示未標記。 標記一個位置,方便以後reset重新從該位置讀取資料。

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

public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

2. 位置(position)

緩衝區中讀取或寫入的下一個位置。這個位置從0開始,最大值等於緩衝區的大小

//獲取緩衝區的位置
public final int position() {
    return position;
}
//設定緩衝區的位置
public final Buffer position(int newPosition) {
    if ((newPosition > limit) || (newPosition < 0))
        throw new IllegalArgumentException();
    position = newPosition;
    if (mark > position) mark = -1;
    return this;
}

3. 限度(limit)

//獲取limit位置
public final int limit() {
    return limit;
}
//設定limit位置
public final Buffer limit(int newLimit) {
    if ((newLimit > capacity) || (newLimit < 0))
        throw new IllegalArgumentException();
    limit = newLimit;
    if (position > limit) position = limit;
    if (mark > limit) mark = -1;
    return this;
 }

4. 容量(capacity)

緩衝區可以儲存元素的最大數量。該值在建立快取區時指定,一旦建立完成後就不能修改該值。

//獲取緩衝區的容量
public final int capacity() {
    return capacity;
}

filp 方法

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}
  1. 將limit設定成當前position的座標
  2. 將position設定為0
  3. 取消標記

rewind 方法

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

從原始碼中發現,rewind修改了position和mark,而沒有修改limit。

  1. 將position設定為0
  2. 取消mark標記

clear 方法

    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
  1. 將position座標設定為0
  2. limit設定為capacity
  3. 取消標記

從clear方法中,我們發現Buffer中的資料沒有清空,如果通過Buffer.get(i)的方式還是可以訪問到資料的。如果再次向緩衝區中寫入資料,他會覆蓋之前存在的資料。

remaining 方法

檢視當前位置和limit之間的元素數。

public final int remaining() {
    return limit - position;
}

hasRemaining 方法

判斷當前位置和limit之間是否還有元素

public final boolean hasRemaining() {
    return position < limit;
}

ByteBuffer 類分析

ByteBuffer類結果圖

從圖中我們可以發現 ByteBuffer繼承於Buffer類,ByteBuffer是個抽象類,它有兩個實現的子類HeapByteBuffer和MappedByteBuffer類

HeapByteBuffer:在堆中建立的緩衝區。就是在jvm中建立的緩衝區。 MappedByteBuffer:直接緩衝區。實體記憶體中建立緩衝區,而不在堆中建立。

allocate 方法(建立堆緩衝區)

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

我們發現allocate方法建立的緩衝區是建立的HeapByteBuffer例項。

HeapByteBuffer 構造

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

從堆緩衝區中看出,所謂堆緩衝區就是在堆記憶體中建立一個byte[]陣列。

allocateDirect建立直接緩衝區

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

我們發現allocate方法建立的緩衝區是建立的DirectByteBuffer例項。

DirectByteBuffer構造

DirectByteBuffer 構造方法

直接緩衝區是通過java中Unsafe類進行在實體記憶體中建立緩衝區。

wrap 方法

public static ByteBuffer wrap(byte[] array)
public static ByteBuffer wrap(byte[] array, int offset, int length);

可以通過wrap類把位元組陣列包裝成緩衝區ByteBuffer例項。 這裡需要注意的的,把array的引用賦值給ByteBuffer物件中位元組陣列。如果array陣列中的值更改,則ByteBuffer中的資料也會更改的。

get 方法

  1. public byte get() 獲取position座標元素,並將position+1;
  2. public byte get(int i) 獲取指定索引下標的元素
  3. public ByteBuffer get(byte[] dst) 從當前position中讀取元素填充到dst陣列中,每填充一個元素position+1;
  4. public ByteBuffer get(byte[] dst, int offset, int length) 從當前position中讀取元素到dst陣列的offset下標開始填充length個元素。

put 方法

  1. public ByteBuffer put(byte x) 寫入一個元素並position+1
  2. public ByteBuffer put(int i, byte x) 指定的索引寫入一個元素
  3. public final ByteBuffer put(byte[] src) 寫入一個自己陣列,並position+陣列長度
  4. public ByteBuffer put(byte[] src, int offset, int length) 從一個自己陣列的offset開始length個元素寫入到ByteBuffer中,並把position+length
  5. public ByteBuffer put(ByteBuffer src) 寫入一個ByteBuffer,並position加入寫入的元素個數

檢視緩衝區

Paste_Image.png

ByteBuffer可以轉換成其它型別的Buffer。例如CharBuffer、IntBuffer 等。

壓縮緩衝區

public ByteBuffer compact() {
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
        position(remaining());
        limit(capacity());
        discardMark();
        return this;
    }

1、把緩衝區positoin到limit中的元素向前移動positoin位 2、設定position為remaining() 3、 limit為緩衝區容量 4、取消標記

例如:ByteBuffer.allowcate(10); 內容:[0 ,1 ,2 ,3 4, 5, 6, 7, 8, 9]

compact前

[0 ,1 ,2 , 3, 4, 5, 6, 7, 8, 9] pos=4 lim=10 cap=10

compact後

[4, 5, 6, 7, 8, 9, 6, 7, 8, 9] pos=6 lim=10 cap=10

slice方法

public ByteBuffer slice() {
        return new HeapByteBuffer(hb,
                    -1,
                    0,
                    this.remaining(),
                    this.remaining(),
                    this.position() + offset);
}

建立一個分片緩衝區。分配緩衝區與主緩衝區共享資料。 分配的起始位置是主緩衝區的position位置 容量為limit-position。 分片緩衝區無法看到主緩衝區positoin之前的元素。

直接緩衝區和堆緩衝區效能對比

下面我們從緩衝區建立的效能和讀取效能兩個方面進行效能對比。

讀寫效能對比

public static void directReadWrite() throws Exception {
    int time = 10000000;
    long start = System.currentTimeMillis();
    ByteBuffer buffer = ByteBuffer.allocate(4*time);
    for(int i=0;i<time;i++){
        buffer.putInt(i);
    }
    buffer.flip();
    for(int i=0;i<time;i++){
        buffer.getInt();
    }
    System.out.println("堆緩衝區讀寫耗時  :"+(System.currentTimeMillis()-start));
    
    start = System.currentTimeMillis();
    ByteBuffer buffer2 = ByteBuffer.allocateDirect(4*time);
    for(int i=0;i<time;i++){
        buffer2.putInt(i);
    }
    buffer2.flip();
    for(int i=0;i<time;i++){
        buffer2.getInt();
    }
    System.out.println("直接緩衝區讀寫耗時:"+(System.currentTimeMillis()-start));
}

輸出結果:

堆緩衝區建立耗時  :70
直接緩衝區建立耗時:47

從結果中我們發現堆緩衝區讀寫比直接緩衝區讀寫耗時更長。

public static void directAllocate() throws Exception {
    int time = 10000000;
    long start = System.currentTimeMillis();
    for (int i = 0; i < time; i++) {
        ByteBuffer buffer = ByteBuffer.allocate(4);
    }
    System.out.println("堆緩衝區建立時間:"+(System.currentTimeMillis()-start));
        
    start = System.currentTimeMillis();
    for (int i = 0; i < time; i++) {
        ByteBuffer buffer = ByteBuffer.allocateDirect(4);
    }
    System.out.println("直接緩衝區建立時間:"+(System.currentTimeMillis()-start));
}

輸出結果:

堆緩衝區建立時間:73
直接緩衝區建立時間:5146

從結果中發現直接緩衝區建立分配空間比較耗時。

對比結論

直接緩衝區比較適合讀寫操作,最好能重複使用直接緩衝區並多次讀寫的操作。 堆緩衝區比較適合建立新的緩衝區,並且重複讀寫不會太多的應用。

建議:如果經過效能測試,發現直接緩衝區確實比堆緩衝區效率高才使用直接緩衝區,否則不建議使用直接緩衝區。

作者:jijs 連結:https://www.jianshu.com/p/451cc865d413 來源:簡書 簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。