Java-NIO之Buffer(緩衝區)

Buffer 是什麼

Buffer(緩衝區)本質上是一個由基本類型數組構成的容器。

我們先看看Buffer類的基本構成:

public abstract class Buffer {
    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
}

再看看子類ByteBuffer 的構成:

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>{
    // These fields are declared here rather than in Heap-X-Buffer in order to
    // reduce the number of virtual method invocations needed to access these
    // values, which is especially costly when coding small buffers.
    //
    final byte[] hb;                  // Non-null only for heap buffers
    final int offset;
    boolean isReadOnly;
}

因此一個ByteBuffer 對象由基本的五大屬性組成:
核心屬性:
● mark 初始值爲-1,用以標記當前position的位置。對應方法爲 mark()。
● position 初始值爲0,讀、寫數據的起點位置。對應方法爲 position()。
● limit 界限,和position 組成可讀、可寫的數據操作區間。對應方法爲 limit()。
● capacity 緩衝區的大小。對應方法爲capacity()。

數據存儲:
● hb 一個基本類型構成的數據,大小等於capacity。

Buffer 如何使用

核心方法:
● put() 寫數據。
● get() 讀數據。
● flip() 翻轉。如當 put 完數據之後,調用flip s 是爲了告知下次 get 數據需要讀取數據區間。反過來也是一樣的道理。

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

● clear() 清空。不會清除數據,但會各個屬性迴歸初始值。

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

● rewind 倒帶。當需要重讀、重寫的時候可以使用。

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

● remaning() 返回剩餘未被處理的數量。

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

假設我們聲明瞭一個 capacity 爲 5 的字節緩衝區:
ByteBuffer buf = ByteBuffer.allocate(4);
那麼,緩衝區的初始狀態就是如下圖所示:
image

Buffer 用來幹什麼

Buffer(緩衝區) 常常用來於NIO的Channel進行交互。數據從緩衝區進行存放和讀取。

1:傳統的IO流讀取、寫入都是直接基於IO流。
2:而使用了buffer後,數據的寫入、讀取是基於buffer,然後再經由IO流進行寫入、讀取。
3:防止內存佔用過大,分段的讀取、寫入數據。


Buffer 讀文件

這裏對比了兩種讀文件的方式。


BIO讀文件(不用Buffer):

    public void ioRead() {
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream(new File("src/test/java/com/loper/mine/SQLParserTest.java"));
            byte[] receive = new byte[8];
            // IO 流讀文件的時候不會管 byte 中的數據是否已被處理過,下一次讀取直接覆蓋
            while (fileInputStream.read(receive) > 0) {
                System.out.println(new String(receive));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fileInputStream != null)
                    fileInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

BufferReader讀文件(用Buffer):

    public void bufferRead() {
        int capacity = 8;
        FileInputStream fileInputStream = null;
        InputStreamReader inputStreamReader = null;
        BufferedReader bufferedReader = null;
        try {
            fileInputStream = new FileInputStream(new File("src/test/java/com/loper/mine/SQLParserTest.java"));
            inputStreamReader = new InputStreamReader(fileInputStream);
            bufferedReader = new BufferedReader(inputStreamReader, capacity);

            CharBuffer receive = CharBuffer.allocate(capacity);
            char[] data = new char[capacity];
            // buffer reader 在讀取數據的時候會判斷buffer 中的數據是否已被清理
            while (bufferedReader.read(receive) > 0) {
                receive.flip();
                receive.get(data);
                receive.flip();
                System.out.println(new String(data));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (bufferedReader != null)
                    bufferedReader.close();
                if (inputStreamReader != null)
                    inputStreamReader.close();
                if (fileInputStream != null)
                    fileInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

可以看到,當我們使用BIO時,從文件流中讀取的數據使用 byte數組接收就可以了。
而使用 BufferReader之後,讀取文件返回的是一個 ByteBuffer,那爲什麼要這麼做呢?
1:使用byte[] 接收數據,我們讀取之後下次再進行寫入的時候是不知道是否已經讀取完畢了的。下次的寫入會將原本的數據直接覆蓋掉。
2:使用ByteBuffer 接收文件流中的數據,在下一次數據寫入前不進行 flip 或 clear 操作,那麼下次寫入數據時並不會更新 ByteBuffer 中的數據。


試想多線程情況下,一個線程寫數據,另一個線程讀數據,若數據還在未確保讀完的情況下就進行下一步寫入了,那麼勢必會丟失數據。
而使用Buffer 則很好的避免了這種情況,無論是寫還是讀,都需要告訴下一次讀或寫數據時的操作區間。byte[] 本身則是不支持這種情況的。


Buffer 與多線程

多線程下模擬數據分段讀、寫:

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 1L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));

        String bufferData = "hello world";
        int capacity = 4;
        // 默認使用分配堆內存分配緩衝區空間(非直接緩衝區)
        //ByteBuffer buffer = ByteBuffer.allocate(capacity);
        // 使用直接內存分配緩衝區空間(直接緩衝區)
        ByteBuffer buffer = ByteBuffer.allocateDirect(capacity);

        Semaphore semaphore1 = new Semaphore(0);
        Semaphore semaphore2 = new Semaphore(0);
        // 寫操作
        executor.execute(() -> {
            int index = 0, len = bufferData.length();
            while (index < len) {
                try {
                    System.out.println("put數據開始----------------");
                    print(buffer);
                    int endIndex = index + capacity;
                    if (endIndex > len)
                        endIndex = len;

                    // 存之前先清空buffer
                    buffer.clear();
                    buffer.put(bufferData.substring(index, endIndex).getBytes());

                    System.out.println("put數據結束----------------");
                    print(buffer);
                    System.out.println("\n");
                    // 存完告訴讀線程可讀區域大小
                    buffer.flip();

                    index += capacity;
                } catch (Exception e) {
                    e.printStackTrace();
                    break;
                } finally {
                    semaphore2.release();
                    try {
                        semaphore1.tryAcquire(3, TimeUnit.SECONDS);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        // 讀操作
        executor.execute(() -> {
            StringBuilder value = new StringBuilder();
            int i = 0;
            while (i < bufferData.length()) {
                try {
                    semaphore2.tryAcquire(3, TimeUnit.SECONDS);
                    System.out.println("get數據開始----------------");
                    print(buffer);

                    byte[] bytes = new byte[buffer.limit()];
                    buffer.get(bytes);
                    value.append(new String(bytes));

                    System.out.println("get數據結束----------------");
                    print(buffer);
                    System.out.println("\n");

                    i += bytes.length;
                } catch (Exception e) {
                    e.printStackTrace();
                    break;
                } finally {
                    semaphore1.release();
                }
            }

            // 完整讀取到的buffer數據
            System.out.println("完整讀取到的buffer數據:" + value.toString());
            buffer.clear();
            print(buffer);
        });

        executor.shutdown();
    }

    private static void print(Buffer buffer) {
        System.out.println("position=" + buffer.position());
        System.out.println("limit   =" + buffer.limit());
        System.out.println("capacity=" + buffer.capacity());
        System.out.println("mark    :" + buffer.mark());
    }

日誌太長,就不全截圖了,如下爲最終輸出:
image

以上代碼模擬了寫線程需要往 buffer 中分段寫入 ‘hello word’,而讀線程則需要從 buffer 中分段讀取,並輸出最終的數據。


個人思考:
從這也聯想到了ftp傳輸數據時也是分段、按序進行傳輸的,也不是一次性將數據一股腦全部丟過去的,這應該就是 Buffer(緩衝區)的作用吧。

Buffer 緩衝區類型

非直接緩衝區

緩衝區空間由JVM內存進行分配。

非直接緩衝區屬於常規操作,傳統的 IO 流和 allocate() 方法分配的緩衝區都是非直接緩衝區,建立在 JVM 內存中。

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

這種常規的非直接緩衝區會將內核地址空間中的內容拷貝到用戶地址空間(中間緩衝區)後再由程序進行讀或寫操作,換句話說,磁盤上的文件在與應用程序交互的過程中會在兩個緩存中來回進行復制拷貝。
如圖:
image

直接緩衝區

緩衝區空間由物理內存直接分配。

直接緩衝區絕大多數情況用於顯著提升性能,緩衝區直接建立在物理內存(相對於JVM 的內存空間)中,省去了在兩個存儲空間中來回複製的操作,可以通過調用 ByteBuffer 的 allocateDirect() 工廠方法來創建。

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

直接緩衝區中的內容可以駐留在常規的垃圾回收堆之外,因此它們對應用程序的內存需求量造成的影響可能並不明顯。
另外,直接緩衝區還可以通過 FileChannel 的 map() 方法將文件直接映射到內存中來創建,
該方法將返回 MappedByteBuffer(DirectByteBuffer extends MappedByteBuffer)。

直接或非直接緩衝區只針對字節緩衝區而言。字節緩衝區是那種類型可以通過 isDirect() 方法來判斷。
如圖:
image

問答區域

1:DirectByteBuffer 比 HeapByteBuffer 更快嗎?

不是。
image

本文參考文章:
1:面試官:Java NIO 的 Buffer 緩衝區,你瞭解多少?
2:Java NIO direct buffer的優勢在哪兒?
3:基於NIO的Socket通信(使用Java NIO的綜合示例講解)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章