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

您好,我是湘王,這是我的博客園,歡迎您來,歡迎您再來~

 

之前在調用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說這麼多已經足夠了,用代碼去感受會更直接。

 


 

 

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