Java I/O(3):NIO中的Buffer

2022-10-20 12:01:20

您好,我是湘王,這是我的部落格園,歡迎您來,歡迎您再來~

 

之前在呼叫Channel的程式碼中,使用了一個名叫ByteBuffer類,它是Buffer的子類。這個叫Buffer的類是專門用來解決高速裝置與低速裝置之間速度不匹配的問題的,也可以減少資料庫的讀寫次數。

它又分為輸入緩衝區和輸出緩衝區。

很多初學者不明白「緩衝」和「快取」的區別,我嘗試著用大白話解釋下:

1、緩衝區需要定期進行重新整理、清空、重置等操作,這些操作快取可能並不需要。比如做飯時,砧板就是緩衝,冰箱就是快取,因為從菜冰箱取出來到下鍋,需要不停地切、拍、剁,每次都要清空了才能做下一道菜,而冰箱是不用定期清空、重置的(除非停電,東西都壞了);

 

 

2、緩衝區核心作用是解耦裝置間的速度制約,成為裝置間的「緩衝」,而快取則是用來加快讀取的速度,減少重新計算或者重新從資料庫獲取的次數。相比於每做一道菜,都從菜場去買,顯然放在冰箱要快得多;而相比於每次做菜都從冰箱拿,當然從砧板上順手拿要更快一些。也就是:「菜場買菜速度(磁碟 < 冰箱拿菜速度(快取 < 砧板拿菜速度(緩衝區)」,就是這麼個關係;

3、緩衝區側重於速度,側重於寫,而快取側重次數,側重於讀。就像砧板側重於切菜,而冰箱側重於存放;

4、現在的快取一般都很大,甚至可以達到TB級別1TB=1024GB),緩衝是不可能這麼大的(當然你也可以把砧板搞成冰箱那麼大,反正我還沒見過這種-_-!)。

 

以後再見到緩衝、快取的時候,就可以拿家裡的砧板和冰箱做對比。

 

NIO中有八種型別的緩衝區:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer, ShortBuffer和MappedByteBuffer,前七種分別對應基本資料型別,MappedByteBuffer專門用於記憶體對映。

 

 

 

緩衝Buffer區實際上也是一個容器,一個由連續陣列/集合組成的容器。Channel提供從檔案、網路讀取資料的渠道,但是讀寫的資料都必須經過Buffer。

Buffer中寫入資料的過程是:

1、Channel寫入資料到Buffer:channel.read(buf)

2、呼叫Buffer的put()方法:buf.put(Object)

Buffer中讀取資料的過程是:

1、Buffer讀取資料到Channel:channel.write(buf)

2、呼叫Buffer的get()方法:buf.get()

讀寫過程大概就是這樣的:

 

 

 

還是昨天那句話:如果你在大廠是自研類RPC系統或類MQ中介軟體的,那這個一定要精通;否則理解就好,不必死磕。Buffer看到這裡其實已經足夠了。至於說:Buffer的屬性、使用Buffer的步驟、JVM怎麼在記憶體建立緩衝區等等,這些應該都是麵霸必修課,但開發中極少用到。

還是用程式碼來說。

Buffer的常用方法:

// 分配JVM間接緩衝區
ByteBuffer buffer = ByteBuffer.allocate(32);
System.out.println("buffer初始狀態: " + buffer);
// 將position設回8
buffer.position(8);
System.out.println("buffer設定後狀態: " + buffer);

System.out.println("測試reset ======================>>>");
// clear()方法,position將被設回0,limit被設定成capacity的值
buffer.clear();
System.out.println("buffer clear後狀態: " + buffer);
// 設定這個緩衝區的位置
buffer.position(5);
// 將此緩衝區的標記設定5
// 如果沒有buffer.mark();這句話會報錯
buffer.mark();
buffer.position(10);
System.out.println("reset前狀態: " + buffer);
// 將此緩衝區的位置重置為先前標記的位置(buffer.position(5))
buffer.reset();
System.out.println("reset後狀態: " + buffer);

System.out.println("測試get ======================>>>");
buffer = ByteBuffer.allocate(32);
buffer.put((byte) 'x').put((byte) 'i').put((byte) 'a').put((byte) 'n').put((byte) 'g');
System.out.println("flip前狀態: " + buffer);
// 轉換為讀模式
buffer.flip();
System.out.println("get前狀態: " + buffer);
System.out.println((char) buffer.get());
System.out.println("get後狀態: " + buffer);

System.out.println("測試put ======================>>>");
ByteBuffer pb = ByteBuffer.allocate(32);
System.out.println("put前狀態: " + pb +
        ", put前資料: " + new String(pb.array()));
System.out.println("put後狀態: " + pb.put((byte) 'w') +
        ", put後資料: " + new String(pb.array()));
System.out.println(pb.put(3, (byte) '3'));
// put(3, (byte) '3')並不改變position的位置,但put((byte) '3')會
System.out.println("put(3, '3')後狀態: " + pb + ", 資料: " + new String(pb.array()));
// 這裡的buffer是 xiang[pos=1 lim=5 cap=32]
System.out.println("buffer疊加前狀態: " + buffer +
        ", buffer疊加前資料: " + new String(buffer.array()));
// buffer.put(pb);會拋異常BufferOverflowException
pb.put(buffer);
// 疊加後資料是wiang,因為buffer的position=1
System.out.println("put(buffer)後bb狀態: " + pb + ", buffer疊加後資料: " + new String(pb.array()));

// 重新讀取buffer中所有資料
System.out.println("測試rewind ======================>>>");
buffer.clear();
buffer.position(10);
System.out.println("buffer當前狀態: " + buffer);
// 返回此緩衝區的限制
buffer.limit(15);
System.out.println("limit後狀態: " + buffer);
// 把position設為0,mark設為-1,不改變limit的值
buffer.rewind();
System.out.println("rewind後狀態: " + buffer);

// 將所有未讀的資料拷貝到Buffer起始處,然後將position設到最後一個未讀元素正後面
System.out.println("測試compact ======================>>>");
buffer.clear();
buffer.put("abcd".getBytes());
System.out.println("compact前狀態: " + buffer);
System.out.println(new String(buffer.array()));
// limit=position;position=0;mark=-1; 翻轉,也就是讓flip之後的position到limit這塊區域變成之前的0到position這塊
// 翻轉就是將一個處於存資料狀態的緩衝區變為一個處於準備取資料的狀態,或者相反
buffer.flip();
System.out.println("flip後狀態: " + buffer);
// get()方法:相對讀,從position位置讀取一個byte,並將position+1,為下次讀寫作準備
System.out.println((char) buffer.get());
System.out.println((char) buffer.get());
System.out.println((char) buffer.get());
System.out.println("三次呼叫get後: " + buffer);
System.out.println(new String(buffer.array()));
// 把從position到limit中的內容移到0到limit-position的區域內
// position和limit的取值也分別變成limit-position、capacity
// 如果先將positon設定到limit,再compact,那麼相當於clear()
buffer.compact();
System.out.println("compact後狀態: " + buffer);
System.out.println(new String(buffer.array()));

 

Java一般用BufferedInputStream、BufferedReader等帶緩衝的I/O類來處理大檔案,但如果檔案超大的話,比如達到GB,甚至TB級別,更快的方式是採用NIO中引入的檔案記憶體對映方案:MappedByteBuffer。

你只需要MappedByteBuffer讀寫效能極高,最主要的原因就是因為它實現了對非同步操作的支援,就可以了!

可以用大檔案來試一下:

// ByteBuffer讀取大檔案
public static void useFileChannel() {
    try{
        FileInputStream fis = new FileInputStream("你電腦上已經存在的檔案路徑,例如C:\\file1");
        FileChannel channel = fis.getChannel();
        long start = System.currentTimeMillis();
        ByteBuffer buff = ByteBuffer.allocate((int) channel.size());
        buff.clear();
        channel.read(buff);
        long end = System.currentTimeMillis();
        System.out.println(end - start);
        fis.close();
        channel.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// MappedByteBuffer讀取大檔案
public static void useMappedByteBuffer() {
    try{
        FileInputStream fis = new FileInputStream("你電腦上已經存在的檔案路徑,例如C:\\file1");
        FileChannel channel = fis.getChannel();
        long start = System.currentTimeMillis();
        MappedByteBuffer mbb = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
        long end = System.currentTimeMillis();
        System.out.println(end - start);
        fis.close();
        channel.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

public static void main(String[] args) {
    useFileChannel();
    useMappedByteBuffer();
}

 

最後把這兩個方法放到main()裡面試試看效果。

 

NIO中的Buffer說這麼多已經足夠了,用程式碼去感受會更直接。

 


 

 

感謝您的大駕光臨!諮詢技術、產品、運營和管理相關問題,請關注後留言。歡迎騷擾,不勝榮幸~