Java NIO:緩衝區

油多壞不了菜發表於2020-10-05

最近打算把Java網路程式設計相關的知識深入一下(IO、NIO、Socket程式設計、Netty)

Java NIO主要需要理解緩衝區、通道、選擇器三個核心概念,作為對Java I/O的補充, 以提升大批量資料傳輸的效率。

學習NIO之前最好能有基礎的網路程式設計知識

Java I/O流

Java 網路程式設計

基礎

除了需要對 Java 網路程式設計有一定了解,還需要對使用者空間、核心空間、記憶體空間多重對映等知識有一定了解

使用者空間與核心空間

為了提供作業系統的穩定性,作業系統將虛擬地址空間分為使用者空間和核心空間

其中使用者程式(我們自己的程式)只能操作使用者空間

I/O過程中的資料流向

假設我們需要從磁碟中某個檔案讀取資料。程式發起read()系統呼叫,進入核心態,核心隨即向磁碟控制硬體發出命令, 要求其從磁碟讀取資料,磁碟控制器將資料直接寫入到核心記憶體緩衝區中(這一步DMA完成,不需要CPU參與),隨後核心把資料從核心空間的臨時緩衝區拷貝到使用者緩衝區(需要CPU參與),程式切換回使用者態繼續執行。

總結起來的資料流向是: 磁碟 ---> 核心緩衝區 ---> 使用者緩衝區

那麼問題來了:核心緩衝區的資料拷貝到使用者緩衝區的這一步顯得有點多餘,是否可以避免?

記憶體空間多重對映

我們知道對於虛擬地址空間,一個以上的虛擬地址可指向同一個實體記憶體地址

如果把使用者空間的虛擬地址和核心空間的虛擬地址對映到同一個實體地址,那麼這塊實體地址代表的空間就對核心和使用者程式都可見了!! 便可省去資料在核心緩衝區和使用者緩衝區來回複製的開銷。(這便是直接緩衝區的思想

緩衝區(Buffer)

Java NIO資料傳輸過程: 資料先放到傳送緩衝區 --> 通過通道傳送到接收端 ---> 接受端通道接受資料並填充到接受緩衝區

所以緩衝區的作用其實是連線通道作為資料傳輸的目標或者來源(或者說緩衝區是通道的輸入或者輸出)

核心概念

屬性

要理解Buffer的工作機制,首先要了解幾個屬性的意義

  • capacity(容量) 緩衝區的容量,建立緩衝區時指定

  • position(位置)下一個要被讀取或者寫入的元素的索引

  • limit(上界) 緩衝區中第一個不能被讀或者寫的位置

  • mark(標記)一個備忘位置

    其中 mark <= position <= limit <= capacity,對limit和position兩個屬性的理解非常重要

存取

緩衝區的核心就在於存取操作,Buffer提供了相對位置存取和絕對位置存取兩種方式

  • 相對位置存取:在當前position位置寫入或者讀取資料, 然後增加position的值

  • 絕對位置存取:在指定的位置寫入或者讀取資料,不改變position的值

 //相對位置存取
 public abstract ByteBuffer put(byte b);
 public abstract byte get();

 //絕對位置存取
 public abstract ByteBuffer put(int index, byte b);
 public abstract byte get(int index);

翻轉(flip)

翻轉是Buffer的核心概念,我們可以理解Buffer有兩種模式:寫模式和讀模式

寫模式下,我們分配一個緩衝區,然後直接填充資料(position的值從0開始遞增);讀模式下, 我們也從頭開始讀取資料(position從0開始遞增)

那麼我們怎麼從寫模式切換到讀模式吶?翻轉!! 翻轉的時候我們用limit記錄待讀取資料的長度, 然後將position置為0 就可以開始讀取資料了

下為翻轉的原始碼

public final Buffer flip() {
  	//記錄待讀取資料的長度
    limit = position;
  	//從頭開始讀取資料
    position = 0;
    mark = -1;
    return this;
}

demo

一個完整的例子

//建立一個緩衝區 
ByteBuffer buffer = ByteBuffer.allocate(100);
//寫資料
for (char c : "hello".toCharArray()) {
  buffer.put((byte) c);
}
//翻轉
buffer.flip();//等價於 buffer.limit(buffer.position()).position(0);
//讀資料
while (buffer.hasRemaining()) {
  char c = (char) buffer.get();
  System.out.println(c);
}

建立緩衝區

Buffer不能直接通過建構函式例項化,都是通過靜態工廠方法來建立。下為ByteBuffer的靜態工廠方法

//建立記憶體緩衝區
public static ByteBuffer allocate(int capacity);
//建立直接緩衝區
public static ByteBuffer allocateDirect(int capacity) ;

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

直接緩衝區(DirectByteBuffer)

對於一般的I/O過程,資料的流向總是:磁碟或者網路 --> 核心臨時緩衝區 --> 使用者空間緩衝區,其中核心空間臨時緩衝區到使用者空間緩衝區複製這一步顯得有點多餘!!

直接緩衝區解決了這個問題, 直接緩衝區對核心和使用者空間都可見,這樣就可以避免"核心臨時緩衝區到使用者空間緩衝區"複製的開銷

雖然直接緩衝區是I/O的最佳選擇,但是其比建立非直接緩衝區花費更大的成本,所以我們對直接緩衝區一般都會重複使用(每次使用都建立的話成本就太高了)。

總結

本文主要講解NIO學習需要掌握的一些基礎知識以及緩衝區的使用,重點是對直接緩衝區的理解。

相關文章