BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 上

知秋z發表於2019-01-22

前言

此係列文章會詳細解讀NIO的功能逐步豐滿的路程,為Reactor-Netty 庫的講解鋪平道路。

關於Java程式設計方法論-Reactor與Webflux的視訊分享,已經完成了Rxjava 與 Reactor,b站地址如下:

Rxjava原始碼解讀與分享:www.bilibili.com/video/av345…

Reactor原始碼解讀與分享:www.bilibili.com/video/av353…

本系列原始碼解讀基於JDK11 api細節可能與其他版本有所差別,請自行解決jdk版本問題。

本系列前幾篇:

BIO到NIO原始碼的一些事兒之BIO

BIO到NIO原始碼的一些事兒之NIO 上

BIO到NIO原始碼的一些事兒之NIO 中

BIO到NIO原始碼的一些事兒之NIO 下 之 Selector

Buffer

在Java BIO中,通過BIO到NIO原始碼的一些事兒之BIO開篇的Demo可知,所有的讀寫API,都是直接使用byte陣列作為緩衝區的,簡單直接。我們來拿一個杯子做例子,我們不講它的材質,只說它的使用屬性,一個杯子在使用過程中會首先看其最大容量,然後加水,這裡給一個限制,即加到杯子中的水量為杯子最大容量的一半,然後喝水,我們最多也只能喝杯子裡所盛水量。由這個例子,我們思考下,杯子是不是可以看作是一個緩衝區,對於杯子倒水的節奏我們是不是可以輕易的控制,從而帶來諸多方便,那是不是可以將之前BIO中的緩衝區也加入一些特性,使之變的和我們使用杯子一樣便捷。 於是,我們給buffer新增幾個屬性,對比杯子的最大容量,我們設計新增一個capacity屬性,對比加上的容量限制,我們設計新增一個limit屬性,對於加水加到杯中的當前位置,我們設計新增一個position屬性,有時候我們還想在杯子上自己做個標記,比如喝茶,我自己的習慣就是喝到杯裡剩三分之一水的時候再加水加到一半,針對這個情況,設計新增一個mark屬性。由此,我們來總結下這幾個屬性的關係,limit不可能比capacity大的,position又不會大於limitmark可以理解為一個標籤,其也不會大於position,也就是mark <= position <= limit <= capacity

結合以上概念,我們來對buffer中這幾個屬性使用時的行為進行下描述:

  • capacity

    也就是緩衝區的容量大小。我們只能往裡面寫capacitybytelongchar等型別。一旦Buffer滿了,需要將其清空(通過讀資料或者清除資料)才能繼續寫資料往裡寫資料。

  • position

    (1)當我們寫資料到Buffer中時,position表示當前的位置。初始的position值為0.當一個bytelongchar等資料寫到Buffer後,position會向前移動到下一個可插入資料的Buffer位置。position最大可為capacity – 1

    (2)當讀取資料時,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式,position會被重置為0. 當從Bufferposition處讀取資料時,position向前移動到下一個可讀的位置。

  • limit

    (1)在寫模式下,Bufferlimit表示你最多能往Buffer裡寫多少資料。寫模式下,limit等於Buffercapacity

    (2)讀模式時,limit表示你最多能讀到多少資料。因此,當切換Buffer到讀模式時,limit會被設定成寫模式下的position值。換句話說,你能讀到之前寫入的所有資料(limit被設定成已寫資料的數量,這個值在寫模式下就是position

  • mark

    類似於喝茶喝到剩餘三分之一誰加水一樣,當buffer呼叫它的reset方法時,當前的位置position會指向mark所在位置,同樣,這個也根據個人喜好,有些人就喜歡將水喝完再新增的,所以mark不一定總會被設定,但當它被設定值之後,那設定的這個值不能為負數,同時也不能大於position。還有一種情況,就是我喝水喝不下了,在最後將水一口喝完,則對照的此處的話,即如果對mark設定了值(並非初始值-1),則在將positionlimit調整為小於mark的值的時候將mark丟棄掉。如果並未對mark重新設定值(即還是初始值-1),那麼在呼叫reset方法會丟擲InvalidMarkException異常。

可見,經過包裝的Buffer是Java NIO中對於緩衝區的抽象。在Java有8中基本型別:byte、short、int、long、float、double、char、boolean,除了boolean型別外,其他的型別都有對應的Buffer具體實現,可見,Buffer是一個用於儲存特定基本資料型別的容器。再加上資料時有序儲存的,而且Buffer有大小限制,所以,Buffer可以說是特定基本資料型別的線性儲存有限的序列。

接著,我們通過下面這幅圖來展示下上面幾個屬性的關係,方便大家更好理解:

BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 上

Buffer的基本用法

先來看一個Demo:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {

  buf.flip();  //make buffer ready for read

  while(buf.hasRemaining()){
      System.out.print((char) buf.get()); // read 1 byte at a time
  }

  buf.clear(); //make buffer ready for writing
  bytesRead = inChannel.read(buf);
}
aFile.close();
複製程式碼

我們拋去前兩行,來總結下buffer的使用步驟:

  1. 通過相應型別Buffer的allocate的靜態方法來分配指定型別大小的緩衝資料區域(此處為buf);
  2. 寫入資料到Buffer;
  3. 呼叫flip()方法:Buffer從寫模式切換到讀模式;
  4. 從buffer讀取資料;
  5. 呼叫clear()方法或則compact()方法。

Buffer分配

那我們依據上面的步驟來一一看下其相應原始碼實現,這裡我們使用ByteBuffer來解讀。首先是Buffer分配。

//java.nio.ByteBuffer#allocate
public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw createCapacityException(capacity);
    return new HeapByteBuffer(capacity, capacity);
}
//java.nio.ByteBuffer#allocateDirect
public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}
複製程式碼

ByteBuffer是一個抽象類,具體的實現有HeapByteBufferDirectByteBuffer。分別對應Java堆緩衝區與堆外記憶體緩衝區。Java堆緩衝區本質上就是byte陣列(由之前分析的,我們只是在位元組陣列上面加點屬性,輔以邏輯,實現一些更復雜的功能),所以實現會比較簡單。而堆外記憶體涉及到JNI程式碼實現,較為複雜,所以我們先來分析HeapByteBuffer的相關操作,隨後再專門分析DirectByteBuffer

我們來看HeapByteBuffer相關構造器原始碼:

//java.nio.HeapByteBuffer#HeapByteBuffer(int, int)
HeapByteBuffer(int cap, int lim) {            

    super(-1, 0, lim, cap, new byte[cap], 0);
    /*
    hb = new byte[cap];
    offset = 0;
    */
    this.address = ARRAY_BASE_OFFSET;
}
//java.nio.ByteBuffer#ByteBuffer(int, int, int, int, byte[], int)
ByteBuffer(int mark, int pos, int lim, int cap,   
                byte[] hb, int offset)
{
    super(mark, pos, lim, cap);
    this.hb = hb;
    this.offset = offset;
}
//java.nio.Buffer#Buffer
Buffer(int mark, int pos, int lim, int cap) {       
    if (cap < 0)
        throw createCapacityException(cap);
    this.capacity = cap;
    limit(lim);
    position(pos);
    if (mark >= 0) {
        if (mark > pos)
            throw new IllegalArgumentException("mark > position: ("
                                                + mark + " > " + pos + ")");
        this.mark = mark;
    }
}
複製程式碼

由上,HeapByteBuffer通過初始化位元組陣列hd,在虛擬機器堆上申請記憶體空間。 因在ByteBuffer中定義有hb這個欄位,它是一個byte[]型別,為了獲取這個欄位相對於當前這個ByteBuffer物件所在記憶體地址,通過private static final long ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(byte[].class)中這個UNSAFE操作來獲取這個陣列第一個元素位置與該物件所在地址的相對長度,這個物件的地址代表你的頭所在的位置,將這個陣列看作你的鼻子,而這裡返回的是你的鼻子距離頭位置的那個長度,即陣列第一個位置距離這個物件開始地址所在位置,這個是在class位元組碼載入到jvm裡的時候就已經確定了。 如果ARRAY_INDEX_SCALE = UNSAFE.arrayIndexScale(byte[].class)為返回非零值,則可以使用該比例因子以及此基本偏移量(ARRAY_BASE_OFFSET)來形成新的偏移量,以訪問這個類的陣列元素。知道這些,在ByteBufferslice duplicate之類的方法,就能理解其操作了,就是計算陣列中每一個元素所佔空間長度得到ARRAY_INDEX_SCALE,然後當我確定我從陣列第5個位置作為該陣列的開始位置操作時,我就可以使用this.address = ARRAY_BASE_OFFSET + off * ARRAY_INDEX_SCALE。 我們再通過下面的原始碼對上述內容對比消化下:

//java.nio.HeapByteBuffer
protected HeapByteBuffer(byte[] buf,
                                int mark, int pos, int lim, int cap,
                                int off)
{
    super(mark, pos, lim, cap, buf, off);
    /*
    hb = buf;
    offset = off;
    */
    this.address = ARRAY_BASE_OFFSET + off * ARRAY_INDEX_SCALE;

}

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


ByteBuffer slice(int pos, int lim) {
    assert (pos >= 0);
    assert (pos <= lim);
    int rem = lim - pos;
return new HeapByteBuffer(hb,
                            -1,
                            0,
                            rem,
                            rem,
                            pos + offset);
}


public ByteBuffer duplicate() {
return new HeapByteBuffer(hb,
                            this.markValue(),
                            this.position(),
                            this.limit(),
                            this.capacity(),
                            offset);
}
複製程式碼

Buffer的讀寫

每個buffer都是可讀的,但不是每個buffer都是可寫的。這裡,當buffer有內容變動的時候,會首先呼叫bufferisReadOnly判斷此buffer是否只讀,只讀buffer是不允許更改其內容的,但markpositionlimit的值是可變的,這是我們人為給其額外的定義,方便我們增加功能邏輯的。當在只讀buffer上呼叫修改時,則會丟擲ReadOnlyBufferException異常。我們來看bufferput方法:

//java.nio.ByteBuffer#put(java.nio.ByteBuffer)
public ByteBuffer put(ByteBuffer src) {
    if (src == this)
        throw createSameBufferException();
    if (isReadOnly())
        throw new ReadOnlyBufferException();
    int n = src.remaining();
    if (n > remaining())
        throw new BufferOverflowException();
    for (int i = 0; i < n; i++)
        put(src.get());
    return this;
}
//java.nio.Buffer#remaining
public final int remaining() {
    return limit - position;
}

複製程式碼

上面remaining方法表示還剩多少資料未讀,上面的原始碼講的是,如果src這個ByteBuffersrc.remaining()的數量大於要存放的目標Buffer的還剩的空間,直接拋溢位的異常。然後通過一個for迴圈,將src剩餘的資料,依次寫入目標Buffer中。接下來,我們通過src.get()來探索下Buffer的讀操作。

//java.nio.HeapByteBuffer#get()
public byte get() {
    return hb[ix(nextGetIndex())];
}

public byte get(int i) {
    return hb[ix(checkIndex(i))];
}
//java.nio.HeapByteBuffer#ix
protected int ix(int i) {
    return i + offset;
}
//java.nio.Buffer#nextGetIndex()
final int nextGetIndex() {                          
    if (position >= limit)
        throw new BufferUnderflowException();
    return position++;
}
複製程式碼

這裡,為了依次讀取陣列中的資料,這裡使用nextGetIndex()來獲取要讀位置,即先返回當前要獲取的位置值,然後position自己再加1。以此在前面ByteBuffer#put(java.nio.ByteBuffer)所示原始碼中的for迴圈中依次對剩餘資料的讀取。上述get(int i)不過是從指定位置獲取資料,實現也比較簡單HeapByteBuffer#ix也只是確定所要獲取此陣列物件指定位置資料,其中的offset表示第一個可讀位元組在該位元組陣列中的位置(就好比我喝茶杯底三分之一水是不喝的,每次都從三分之一水量開始位置計算喝了多少或者加入多少水)。 接下來看下單個位元組儲存到指定位元組陣列的操作,與獲取位元組陣列單個位置資料相對應,程式碼比較簡單:

//java.nio.HeapByteBuffer#put(byte)
public ByteBuffer put(byte x) {

    hb[ix(nextPutIndex())] = x;
    return this;
}

public ByteBuffer put(int i, byte x) {

    hb[ix(checkIndex(i))] = x;
    return this;
}
//java.nio.Buffer#nextPutIndex()
final int nextPutIndex() {                          // package-private
    if (position >= limit)
        throw new BufferOverflowException();
    return position++;
}
複製程式碼

前面的都是單個位元組的,下面來講下批量操作位元組陣列是如何進行的,因過程知識點重複,這裡只講get,先看原始碼:

//java.nio.ByteBuffer#get(byte[])
public ByteBuffer get(byte[] dst) {
    return get(dst, 0, dst.length);
}
//java.nio.ByteBuffer#get(byte[], int, int)
public ByteBuffer get(byte[] dst, int offset, int length) {
    // 檢查引數是否越界
    checkBounds(offset, length, dst.length);
     // 檢查要獲取的長度是否大於Buffer中剩餘的資料長度
    if (length > remaining())
        throw new BufferUnderflowException();
    int end = offset + length;
    for (int i = offset; i < end; i++)
        dst[i] = get();
    return this;
}
//java.nio.Buffer#checkBounds
static void checkBounds(int off, int len, int size) { // package-private
    if ((off | len | (off + len) | (size - (off + len))) < 0)
        throw new IndexOutOfBoundsException();
}
複製程式碼

通過這個方法將這個buffer中的位元組資料讀到我們給定的目標陣列dst中,由checkBounds可知,當要寫入目標位元組陣列的可寫長度小於將要寫入資料的長度的時候,會產生邊界異常。當要獲取的長度是大於Buffer中剩餘的資料長度時丟擲BufferUnderflowException異常,當驗證通過後,接著就從目標陣列的offset位置開始,從buffer獲取並寫入offset + length長度的資料。 可以看出,HeapByteBuffer是封裝了對byte陣列的簡單操作。對緩衝區的寫入和讀取本質上是對陣列的寫入和讀取。使用HeapByteBuffer的好處是我們不用做各種引數校驗,也不需要另外維護陣列當前讀寫位置的變數了。 同時我們可以看到,Buffer中對position的操作沒有使用鎖保護,所以Buffer不是執行緒安全的。如果我們操作的這個buffer會有多個執行緒使用,則針對該buffer的訪問應通過適當的同步控制機制來進行保護。

ByteBuffer的模式

jdk本身是沒這個說法的,只是按照我們自己的操作習慣,我們將Buffer分為兩種工作模式,一種是接收資料模式,一種是輸出資料模式。我們可以通過Buffer提供的flip等操作來切換Buffer的工作模式。

我們來新建一個容量為10的ByteBuffer:

ByteBuffer.allocate(10);
複製程式碼

由前面所學的HeapByteBuffer的構造器中的相關程式碼可知,這裡的position被設定為0,而且 capacitylimit設定為 10,mark設定為-1,offset設定為0。 可參考下圖展示:

HeapByteBuffer初始化

新建的Buffer處於接收資料的模式,可以向Buffer放入資料,在放入一個對應基本型別的資料後(此處假如放入一個char型別資料),position加一,參考我們上面所示原始碼,如果position已經等於limit了還進行put操作,則會丟擲BufferOverflowException異常。 我們向所操作的buffer中put 5個char型別的資料進去:

buffer.put((byte)'a').put((byte)'b').put((byte)'c').put((byte)'d').put((byte)'e');
複製程式碼

會得到如下結果檢視:

BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 上

由之前原始碼分析可知,Buffer的讀寫的位置變數都是基於position來做的,其他的變數都是圍繞著它進行輔助管理的,所以如果從Buffer中讀取資料,要將Buffer切換到輸出資料模式(也就是讀模式)。此時,我們就可以使用Buffer提供了flip方法。

//java.nio.Buffer#flip
public Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}
複製程式碼

我們知道,在put的時候,會進行java.nio.Buffer#nextPutIndex()的呼叫,裡面會進行position >= limit,所以,此時再進行寫操作的話,會從第0個位置開始進行覆蓋,而且只能寫到flip操作之後limit的位置。

//java.nio.Buffer#nextPutIndex()
final int nextPutIndex() {                          // package-private
    if (position >= limit)
        throw new BufferOverflowException();
    return position++;
}
複製程式碼

在做完put操作後,position會自增一下,所以,flip操作示意圖如下:

BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 上

也是因為position為0了,所以我們可以很方便的從Buffer中第0個位置開始讀取資料,不需要別的附加操作。由之前解讀可知,每次讀取一個元素,position就會加一,如果position已經等於limit還進行讀取,則會丟擲BufferUnderflowException異常。

我們通過flip方法把Buffer從接收寫模式切換到輸出讀模式,如果要從輸出模式切換到接收模式,可以使用compact或者clear方法,如果資料已經讀取完畢或者資料不要了,使用clear方法,如果只想從緩衝區中釋放一部分資料,而不是全部(即釋放已讀資料,保留未讀資料),然後重新填充,使用compact方法。

對於clear方法,我們先來看它的原始碼:

//java.nio.Buffer#clear
public Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}
複製程式碼

我們可以看到,它的clear方法內並沒有做清理工作,只是修改位置變數,重置為初始化時的狀態,等待下一次將資料寫入緩衝陣列。 接著,來看compact操作的原始碼:

//java.nio.HeapByteBuffer#compact
public ByteBuffer compact() {
    System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
    position(remaining());
    limit(capacity());
    discardMark();
    return this;
}
//java.nio.ByteBuffer#position
ByteBuffer position(int newPosition) {
    super.position(newPosition);
    return this;
}
//java.nio.Buffer#position(int)
public Buffer position(int newPosition) {
    if (newPosition > limit | newPosition < 0)
        throw createPositionException(newPosition);
    position = newPosition;
    if (mark > position) mark = -1;
    return this;
}
//java.nio.ByteBuffer#limit
ByteBuffer limit(int newLimit) {
    super.limit(newLimit);
    return this;
}
//java.nio.Buffer#limit(int)
public Buffer limit(int newLimit) {
    if (newLimit > capacity | newLimit < 0)
        throw createLimitException(newLimit);
    limit = newLimit;
    if (position > limit) position = limit;
    if (mark > limit) mark = -1;
    return this;
}
//java.nio.Buffer#discardMark
final void discardMark() {                          
        mark = -1;
    }
複製程式碼

這裡使用了陣列的拷貝操作,將未讀元素轉移到該位元組陣列從0開始的位置,由於remaining()返回的是limit - position,假如在flip操作的時候填入的元素有5個,那麼limit5,此時讀到了第三個元素,也就是在呼叫compactposition的數值為2,那remaining()的值就為3,也就是此時position3compact操作後,limit會迴歸到和初始化陣列容量大小一樣,並將mark值置為 -1

我們來看示意圖,在進行buffer.compact()呼叫前:

BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 上

buffer.compact()呼叫後:

BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 上

ByteBuffer的其他方法

接下來,我們再接觸一些ByteBuffer的其他方法,方便在適當的條件下進行使用。

rewind方法

首先來看它的原始碼:

//java.nio.Buffer#rewind
public Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}
複製程式碼

這裡就是將position設定為0,mark設定為-1,其他設定的管理屬性(capacitylimit)不變。結合前面的知識,在位元組陣列寫入資料後,它的clear方法也只是重置我們在Buffer中設定的那幾個增強管理屬性(capacitypositionlimitmark),此處的英文表達的意思也很明顯:倒帶,也就是可以回頭重新寫,或者重新讀。但是我們要注意一個前提,我們要確保已經恰當的設定了limit。這個方法可以在Channel的讀或者寫之前呼叫,如:

out.write(buf);    // Write remaining data
buf.rewind();      // Rewind buffer
buf.get(array);    // Copy data into array
複製程式碼

我們通過下圖來進行展示執行rewind操作後的結果:

BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 上

duplicate 方法

在JDK9版本中,新增了這個方法。用來建立一個與原始Buffer一樣的新Buffer。新Buffer的內容和原始Buffer一樣。改變新Buffer內的資料,同樣會體現在原始Buffer上,反之亦然。兩個Buffer都擁有自己獨立的 positionlimitmark 屬性。 剛建立的新Bufferpositionlimitmark 屬性與原始Buffer對應屬性的值相同。 還有一點需要注意的是,如果原始Buffer是隻讀的(即HeapByteBufferR),那麼新Buffer也是隻讀的。如果原始BufferDirectByteBuffer,那新Buffer也是DirectByteBuffer。 我們來看相關原始碼實現:

//java.nio.HeapByteBuffer#duplicate
public ByteBuffer duplicate() {
    return new HeapByteBuffer(hb,
                                this.markValue(),
                                this.position(),
                                this.limit(),
                                this.capacity(),
                                offset);
}
//java.nio.HeapByteBufferR#duplicate
public ByteBuffer duplicate() {
    return new HeapByteBufferR(hb,
                                this.markValue(),
                                this.position(),
                                this.limit(),
                                this.capacity(),
                                offset);
}
//java.nio.DirectByteBuffer#duplicate
public ByteBuffer duplicate() {
    return new DirectByteBuffer(this,
                                    this.markValue(),
                                    this.position(),
                                    this.limit(),
                                    this.capacity(),
                                    0);
}

複製程式碼

基本型別的引數傳遞都是值傳遞,所以由上面原始碼可知每個新緩衝區都擁有自己的 positionlimitmark 屬性,而且他們的初始值使用了原始Buffer此時的值。 但是,從HeapByteBuffer角度來說,對於hb 作為一個陣列物件,屬於物件引用傳遞,即新老Buffer共用了同一個位元組陣列物件。無論誰操作,都會改變另一個。 從DirectByteBuffer角度來說,直接記憶體看重的是地址操作,所以,其在建立這個新Buffer的時候傳入的是原始Buffer的引用,進而可以獲取到相關地址。

asReadOnlyBuffer

可以使用 asReadOnlyBuffer() 方法來生成一個只讀的緩衝區。這與 duplicate()實現有些相同,除了這個新的緩衝區不允許使用put(),並且其isReadOnly()函式 將會返回true 。 對這一隻讀緩衝區呼叫put()操作,會導致ReadOnlyBufferException異常。 我們來看相關原始碼:

//java.nio.ByteBuffer#put(java.nio.ByteBuffer)
public ByteBuffer put(ByteBuffer src) {
    if (src == this)
        throw createSameBufferException();
    if (isReadOnly())
        throw new ReadOnlyBufferException();
    int n = src.remaining();
    if (n > remaining())
        throw new BufferOverflowException();
    for (int i = 0; i < n; i++)
        put(src.get());
    return this;
}
//java.nio.HeapByteBuffer#asReadOnlyBuffer
public ByteBuffer asReadOnlyBuffer() {

    return new HeapByteBufferR(hb,
                                    this.markValue(),
                                    this.position(),
                                    this.limit(),
                                    this.capacity(),
                                    offset);
}
//java.nio.HeapByteBufferR#asReadOnlyBuffer
//HeapByteBufferR下直接呼叫其duplicate方法即可,其本來就是隻讀的
public ByteBuffer asReadOnlyBuffer() {
    return duplicate();
}
//java.nio.DirectByteBuffer#asReadOnlyBuffer
public ByteBuffer asReadOnlyBuffer() {

    return new DirectByteBufferR(this,
                                        this.markValue(),
                                        this.position(),
                                        this.limit(),
                                        this.capacity(),
                                        0);
}
//java.nio.DirectByteBufferR#asReadOnlyBuffer
public ByteBuffer asReadOnlyBuffer() {
    return duplicate();
}

//java.nio.HeapByteBufferR#HeapByteBufferR
protected HeapByteBufferR(byte[] buf,
                int mark, int pos, int lim, int cap,
                int off)
{
    super(buf, mark, pos, lim, cap, off);
    this.isReadOnly = true;

}
//java.nio.DirectByteBufferR#DirectByteBufferR
DirectByteBufferR(DirectBuffer db,       
                int mark, int pos, int lim, int cap,
                int off)
{

    super(db, mark, pos, lim, cap, off);
    this.isReadOnly = true;

}
複製程式碼

可以看到,ByteBuffer的只讀實現,在構造器裡首先將isReadOnly屬性設定為true。接著,HeapByteBufferR繼承了HeapByteBuffer 類(DirectByteBufferR也是類似實現,就不重複了),並重寫了所有可對buffer修改的方法。把所有能修改buffer的方法都直接丟擲ReadOnlyBufferException來保證只讀。來看DirectByteBufferR相關原始碼,其他對應實現一樣:

//java.nio.DirectByteBufferR#put(byte)
public ByteBuffer put(byte x) {
    throw new ReadOnlyBufferException();
}
複製程式碼
slice 方法

slice從字面意思來看,就是切片,用在這裡,就是分割ByteBuffer。即建立一個從原始ByteBuffer的當前位置(position)開始的新ByteBuffer,並且其容量是原始ByteBuffer的剩餘消費元素數量( limit-position)。這個新ByteBuffer與原始ByteBuffer共享一段資料元素子序列,也就是設定一個offset值,這樣就可以將一個相對陣列第三個位置的元素看作是起點元素,此時新ByteBufferposition就是0,讀取的還是所傳入這個offset的所在值。分割出來的ByteBuffer也會繼承只讀和直接屬性。 我們來看相關原始碼:

//java.nio.HeapByteBuffer#slice()
public ByteBuffer slice() {
    return new HeapByteBuffer(hb,
                                -1,
                                0,
                                this.remaining(),
                                this.remaining(),
                                this.position() + offset);
}
protected HeapByteBuffer(byte[] buf,
                    int mark, int pos, int lim, int cap,
                    int off)
{
    super(mark, pos, lim, cap, buf, off);
    /*
    hb = buf;
    offset = off;
    */
    this.address = ARRAY_BASE_OFFSET + off * ARRAY_INDEX_SCALE;
}
複製程式碼

由原始碼可知,新ByteBuffer和原始ByteBuffer共有了一個陣列,新ByteBuffermark值為-1,position值為0,limitcapacity都為原始Bufferlimit-position的值。 於是,我們可以通過下面兩幅圖來展示slice方法前後的對比。

原始ByteBuffer

BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 上

呼叫slice方法分割後得到的新ByteBuffer

BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 上

本篇到此為止,在下一篇中,我會著重講下DirectByteBuffer的實現細節。

本文參考及圖片來源:www.jianshu.com/p/12c81abb5…

相關文章