1. 程式人生 > 其它 >【NIO】Buffer:基本原理及高階使用

【NIO】Buffer:基本原理及高階使用

技術標籤:IO、RPC框架程式語言buffer

緩衝區實際上是一個容器物件,更直接的說,其實就是一個陣列,在 NIO 庫中,所有資料都是用緩衝區處理的。

讀/寫 ==> Buffer。即使用者的直接操作都是面向緩衝區。在讀取資料時,它是直接讀到緩衝區中的; 在寫入資料時,它也是寫入到緩衝區中的;任何時候訪問 NIO 中的資料,都是將它放到緩衝區中。

PS:在面向流I/O 系統中,所有資料都是直接寫入或者直接將資料讀取到 Stream 物件中。

在NIO 中,所有的緩衝區型別都繼承於抽象類 Buffer,最常用的就是 ByteBuffer,對於 Java 中的基本型別,基本都有一個具體 Buffer 型別與之相對應,它們之間的繼承關係如下圖所示:

在這裡插入圖片描述
下面來看一個使用 Buffer 的簡單示例,其中包含了最基本的 API:

public class IntBufferDemo { 
    public static void main(String[] args) {
        // 1.分配新的 int 緩衝區,引數為緩衝區容量(capacity) 
        // 新緩衝區的當前位置將為零,其界限(limit)將為其容量。
        // 它將具有一個底層實現陣列,其陣列偏移量(position)將為零。 
        IntBuffer buffer = IntBuffer.allocate(8);
        
	    for
(int i = 0; i < buffer.capacity(); ++i) { int j = 2 * (i + 1); // 2.將給定整數寫入此緩衝區的當前位置,當前位置遞增 // 注:因為底層是陣列,所以還可以 put(index,value) buffer.put(j); } // 3.重設此緩衝區,將限制設定為當前位置,然後將當前位置設定為 0 buffer.flip(); // 檢視在當前位置和限制位置之間是否有元素
// 注:這裡也可以 buffer.remaining() > 0 while (buffer.hasRemaining()) { // 4.讀取此緩衝區當前位置的整數,然後當前位置遞增 // 注:因為底層是陣列,所以還可以 get(index) int j = buffer.get(); System.out.print(j + " "); } } }

在這裡插入圖片描述

1.Buffer 基本原理

在談到緩衝區時,我們說緩衝區物件本質上是一個數組,但它其實是一個特殊的陣列,緩衝區物件內建了一些機制,
能夠跟蹤和記錄緩衝區的狀態變化情況,如果我們使用 get()方法從緩衝區獲取資料或者使用 put()方法把資料寫入緩衝
區,都會引起緩衝區狀態的變化。

在緩衝區中,最重要的屬性有下面三個,它們一起合作完成對緩衝區內部狀態的變化跟蹤:

  • position:遊標,指定下一個將要被寫入或者讀取的元素索引,它的值由 get()/put()方法自動更新,在新建立一個 Buffer 物件時,position 被初始化為0。
  • limit:界限,指定還有多少資料需要取出(從緩衝區寫入通道時),或者還有多少空間可以放入資料(從通道讀入緩衝區時)。
  • capacity:容量,指定了可以儲存在緩衝區中的最大資料容量,實際上,它指定了底層陣列的大小,或者至少是指定了准許我們使用的底層陣列的容量。

以上三個屬性值之間有一些相對大小的關係:0 <=position <= limit <=capacity

如果我們建立一個新的容量大小為 10 的 ByteBuffer 物件,在初始化的時候,position 設定為0,limit 和 capacity 被設定為 10,在以後使用 ByteBuffer 物件過程中,capacity 的值不會再發生變化,而其它兩個將會隨著使用而變化。

下面我們用程式碼來演示一遍,準備一個txt文件,存放的 C 盤,輸入以下內容:

Zhangsan

下面我們用一段程式碼來驗證 position、limit和 capacity 這幾個值的變化過程,程式碼如下

public class BufferDemo {

    public static void main(String[] args) throws Exception {
        FileInputStream fin = new FileInputStream("C://test.txt");
        // 1.建立檔案操作管道
        // 注:BIO中沒有這一步
        FileChannel fc = fin.getChannel();
	    
        // 2.建立緩衝區(Buffer)
        // 初始化容量為10,就是建立一個大小為10的byte陣列
        ByteBuffer buffer = ByteBuffer.allocate(10);
        output("初始化", buffer);
        
		// 3.將管道中的資料讀到Buffer中
        fc.read(buffer);
        output("呼叫read()", buffer);
		
        // 4.準備操作之前,先鎖定操作範圍
        buffer.flip();
        output("呼叫flip()", buffer);
		
        // 5.讀取Buffer中的資料
        // 注:這裡是逐位元組讀取
        while (buffer.remaining() > 0) {
            byte b = buffer.get();
        }
        output("呼叫get()", buffer);
		
        // 6.clear可以理解為解鎖
        buffer.clear();
        output("呼叫clear()", buffer);

        fin.close();
    }

    private static void output(String step, ByteBuffer buffer) {
        System.out.println(step + " : ");
        // capacity,容量
        System.out.print("capacity: " + buffer.capacity() + ", ");
        // position,遊標,記錄要操作資料位置
        System.out.print("position: " + buffer.position() + ", ");
        // limit,界限,資料操作範圍在position-limit之間
        System.out.print("limit: " + buffer.limit());
        System.out.println("\n");
    }
}

輸出結果:

1)初始化,limit=capacitty,position=0

在這裡插入圖片描述

2) read(),讀取資料進Buffer,position=資料大小
在這裡插入圖片描述

3)filp(),鎖定,將limit移動到position,將position置0

在這裡插入圖片描述

4)get(),逐位讀取Buffer中的資料,positon向limit移動,且移動範圍不超過limit

在這裡插入圖片描述

5)clear(),解鎖,position=0,limit=capacity

在這裡插入圖片描述

2.Buffer 高階使用

在文章開篇我們演示了的 Buffer 的基本使用,下面我們來看看關於 Buffer 的一些高階用法…

2.1 緩衝區分配

在前面的幾個例子中,我們已經看到了,在建立一個緩衝區物件時,會呼叫靜態方法allocate()來指定緩衝區的容量,其實呼叫allocate()相當於建立了一個指定大小的陣列,並把它包裝為緩衝區物件。

或者我們也可以直接將一個現有的陣列,包裝為緩衝區物件,如下示例程式碼所示:

public Buffer wrap() {
        // 方式一:分配指定大小緩衝區
        ByteBuffer buffer1 = ByteBuffer.allocate(10);
		
        // 方式二:手動包裝一個現有陣列
        byte[] arr = new byte[10];
        ByteBuffer buffer2 = ByteBuffer.wrap(arr);
        
        return buffer2;
    }

2.2 緩衝區分片

在NIO中,除了可以分配或者包裝一個緩衝區物件外,還可以根據現有的緩衝區物件來建立一個子緩衝區,即在現有緩衝區上切出一片來作為一個新的緩衝區,但現有的緩衝區與建立的子緩衝區在底層陣列層面上是資料共享的。也就是說,子緩衝區相當於是現有緩衝區的一個檢視視窗。

呼叫slice()方法可以建立一個子緩衝區,讓我們通過例子來看一下:

public static void main(String[] args) {
    ByteBuffer buffer = ByteBuffer.allocate(10);
	
    for (int i = 0; i < buffer.capacity(); i++) {
        // 注:這裡要將int強轉為byte
        buffer.put((byte)i);
    }
	
    // 建立[3, 7)的分片
    // 注:建立分片的方式是[position,limit),
    //     可以通過buffer.capacity()/limit()手動設定
    buffer.position(3);
    buffer.limit(7);
    ByteBuffer slice = buffer.slice();
    
	// 改變分片中的資料  ----> 實際上是改變的原陣列
    for (int i = 0; i < slice.capacity(); i++) {
        byte b = slice.get(i);
        b *= 10;
        slice.put(i, b);
    }
	
    // 注:因為修改了p,l,所以在最後讀去原陣列時要將position與limit還原
    buffer.position(0);
    buffer.limit(buffer.capacity());

    while (buffer.hasRemaining()) {
        System.out.print(buffer.get());
    }

}

在這裡插入圖片描述

2.3 只讀緩衝區

只讀緩衝區非常簡單,可以讀取它們,但是不能向它們寫入資料。

可以通過呼叫緩衝區的 asReadOnlyBuffer()方法,將任何常規緩衝區轉換為只讀緩衝區,這個方法返回一個與原緩衝區完全相同的緩衝區,並與原緩衝區共享資料,只不過它是隻讀的。

PS:如果原緩衝區的內容發生了變化,只讀緩衝區的內容也隨之發生變化

public class ReadOnlyBuffer {

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);

        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put((byte)i);
        }
	   
        // 通過原Buffer建立只讀Buffer
        ByteBuffer readOnly = buffer.asReadOnlyBuffer();
		
        // 修改原Buffer
        for (int i = 0; i < buffer.capacity(); i++) {
            byte b = buffer.get(i);
            b *= 10;
            buffer.put(i, b);
        }

        readOnly.position(0);
        readOnly.limit(buffer.capacity());
		
        // 只讀Buffer隨之改變
        while (buffer.hasRemaining()) {
            System.out.println(readOnly.get());
        }
    }
}

只讀緩衝區對於保護資料很有用。在將緩衝區傳遞給某個物件的方法時,無法知道這個方法是否會修改緩衝區中的資料。如果嘗試修改只讀緩衝區的內容,則會報ReadOnlyBufferException異常。

建立一個只讀的緩衝區可以保證該緩衝區不會被修改。只可以把常規緩衝區轉換為只讀緩衝區,而不能將只讀的緩衝區轉換為可寫的緩衝區。

2.4 直接緩衝區

直接緩衝區是為加快I/O 速度,使用一種特殊方式為其分配記憶體的緩衝區,JDK文件中的描述為:給定一個直接位元組緩衝區,Java虛擬機器將盡最大努力直接對它執行本機I/O操作。

也就是說,它會在每一次呼叫底層作業系統的本機I/O 操作之前(或之後),嘗試避免將緩衝區的內容拷貝到一箇中間緩衝區(JVM的)或者從一箇中間緩衝區中拷貝資料。

要分配直接緩衝區,需要呼叫allocateDirect()方法,而不是allocate()方法,使用方式與普通緩衝區並無區別,如下面的拷貝檔案示例:

public class DirectBuffer { 
    static public void main( String args[] ) throws Exception {
		// 首先我們從磁碟上讀取剛才我們寫出的檔案內容 
        String infile = "C://test.txt"; 
        FileInputStream fin = new FileInputStream( infile ); 
        FileChannel fcin = fin.getChannel();
        
		// 把剛剛讀取的內容寫入到一個新的檔案中 
        String outfile = String.format("C://testcopy.txt"); 
        FileOutputStream fout = new FileOutputStream( outfile ); 
        FileChannel fcout = fout.getChannel();
        
		// 使用 allocateDirect,而不是 allocate 
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        
        while (true) { 
            buffer.clear();
            	
        	int r = fcin.read(buffer);
            if (r==-1) { 
                break; 
            }
        	buffer.flip();
        	fcout.write(buffer);
        }
	}
}

2.5 記憶體對映

記憶體對映是一種讀和寫檔案資料的方法,它可以比常規的基於流或者基於通道的I/O 快的多。記憶體對映檔案I/O 是通過使檔案中的資料出現為記憶體陣列的內容來完成的,這其初聽起來似乎不過就是將整個檔案讀到記憶體中,但是事實上並不是這樣。一般來說,只有檔案中實際讀取或者寫入的部分才會對映到記憶體中。如下面的示例程式碼:

public class MappedBuffer { 
    static private final int start = 0; 
    static private final int size = 1024;
    
	static public void main( String args[] ) throws Exception { 
        RandomAccessFile raf = new RandomAccessFile( "C://test.txt", "rw" );
        FileChannel fc = raf.getChannel();
        
	    // 把緩衝區跟檔案系統進行一個對映關聯 
        // 只要操作緩衝區裡面的內容,檔案內容也會跟著改變 
        MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE,start, size );
	    mbb.put( 0, (byte)97 ); 
        mbb.put( 1023, (byte)122 );
        
	    raf.close();
	}
}