Java NIO之Buffer(緩沖區(qū))

Java NIO主要解決了Java IO的效率問題,解決此問題的思路之一是利用硬件和操作系統(tǒng)直接支持的緩沖區(qū)染乌、虛擬內(nèi)存俺榆、磁盤控制器直接讀寫等優(yōu)化IO的手段;思路之二是提供新的編程架構(gòu)使得單個線程可以控制多個IO粒蜈,從而節(jié)約線程資源顺献,提高IO性能。
Java IO引入了三個主要概念枯怖,即緩沖區(qū)(Buffer)注整、通道(Channel)和選擇器(Selector),本文主要介紹緩沖區(qū)度硝。

  1. 緩沖區(qū)概念
    緩沖區(qū)是對Java原生數(shù)組的對象封裝肿轨,它除了包含其數(shù)組外,還帶有四個描述緩沖區(qū)特征的屬性以及一組用來操作緩沖區(qū)的API蕊程。緩沖區(qū)的根類是Buffer椒袍,其重要的子類包括ByteBuffer、MappedByteBuffer藻茂、CharBuffer驹暑、IntBuffer、DoubleBuffer辨赐、ShortBuffer优俘、LongBuffer、FloatBuffer掀序。從其名稱可以看出這些類分別對應了存儲不同類型數(shù)據(jù)的緩沖區(qū)帆焕。

1.1四個屬性
緩沖區(qū)由四個屬性指明其狀態(tài)。
容量(Capacity):緩沖區(qū)能夠容納的數(shù)據(jù)元素的最大數(shù)量森枪。初始設定后不能更改视搏。
上界(Limit):緩沖區(qū)中第一個不能被讀或者寫的元素位置审孽。或者說浑娜,緩沖區(qū)內(nèi)現(xiàn)存元素的上界佑力。
位置(Position):緩沖區(qū)內(nèi)下一個將要被讀或?qū)懙脑匚恢谩T谶M行讀寫緩沖區(qū)時筋遭,位置會自動更新打颤。
標記(Mark):一個備忘位置。初始時為“未定義”漓滔,調(diào)用mark時mark=positon编饺,調(diào)用reset時position=mark。
這四個屬性總是滿足如下關(guān)系:

mark<=position<=limit<=capacity

如果我們創(chuàng)建一個新的容量大小為10的ByteBuffer對象如下圖所示:

image.png

在初始化的時候响驴,position設置為0透且,limit和 capacity被設置為10,在以后使用ByteBuffer對象過程中豁鲤,capacity的值不會再發(fā)生變化秽誊,而其它兩個個將會隨著使用而變化。三個屬性值分別如圖所示:


image.png

現(xiàn)在我們可以從通道中讀取一些數(shù)據(jù)到緩沖區(qū)中琳骡,注意從通道讀取數(shù)據(jù)锅论,相當于往緩沖區(qū)中寫入數(shù)據(jù)。如果讀取4個字節(jié)的數(shù)據(jù)楣号,則此時position的值為4最易,即下一個將要被寫入的字節(jié)索引為4,而limit仍然是10炫狱,如下圖所示:


image.png

下一步把讀取的數(shù)據(jù)寫入到輸出通道中藻懒,相當于從緩沖區(qū)中讀取數(shù)據(jù),在此之前视译,必須調(diào)用flip()方法束析,該方法將會完成兩件事情:

  1. 把limit設置為當前的position值
  2. 把position設置為0
    由于position被設置為0,所以可以保證在下一步輸出時讀取到的是緩沖區(qū)中的第一個字節(jié)憎亚,而limit被設置為當前的position员寇,可以保證讀取的數(shù)據(jù)正好是之前寫入到緩沖區(qū)中的數(shù)據(jù),如下圖所示:


    image.png

現(xiàn)在調(diào)用get()方法從緩沖區(qū)中讀取數(shù)據(jù)寫入到輸出通道第美,這會導致position的增加而limit保持不變蝶锋,但position不會超過limit的值,所以在讀取我們之前寫入到緩沖區(qū)中的4個自己之后什往,position和limit的值都為4扳缕,如下圖所示:


image.png

在從緩沖區(qū)中讀取數(shù)據(jù)完畢后,limit的值仍然保持在我們調(diào)用flip()方法時的值,調(diào)用clear()方法能夠把所有的狀態(tài)變化設置為初始化時的值躯舔,如下圖所示:


image.png

下面這個例子可以展示buffer的讀寫:

public class NioTest1 {

    public static void main(String[] args) {
        //通過nio生成隨機數(shù)驴剔,然后在打印出來

        IntBuffer buffer = IntBuffer.allocate(10);
        System.out.println("capacity:"+buffer.capacity());
        for (int i = 0;i < 5;i++){
            int randomNumber = new SecureRandom().nextInt(20);
            //這里相當于把數(shù)據(jù)寫到buffer中
            buffer.put(randomNumber);
        }

        System.out.println("before flip limit:"+buffer.capacity());

        //上面是寫,下面為讀粥庄,通過flip()方法進行讀寫的切換
        buffer.flip();

        System.out.println("after flip limit:"+buffer.capacity());

        System.out.println("enter while loop");

        while(buffer.hasRemaining()){
            System.out.println("position:" + buffer.position());
            System.out.println("limit:" + buffer.limit());
            System.out.println("capacity:" + buffer.capacity());

            //這里相當于從buffer中讀出數(shù)據(jù)
            System.out.println(buffer.get());
        }

    }

1.3 remaining和hasRemaining
remaining()會返回緩沖區(qū)中目前存儲的元素個數(shù)丧失,在使用參數(shù)為數(shù)組的get方法中,提前知道緩沖區(qū)存儲的元素個數(shù)是非常有用的惜互。
事實上布讹,由于緩沖區(qū)的讀或者寫模式并不清晰,因此實際上remaining()返回的僅僅是limit – position的值训堆。
而hasRemaining()的含義是查詢緩沖區(qū)中是否還有元素描验,這個方法的好處是它是線程安全的。

1.4 Flip翻轉(zhuǎn)
在從緩沖區(qū)中讀取數(shù)據(jù)時坑鱼,get方法會從position的位置開始膘流,依次讀取數(shù)據(jù),每次讀取后position會自動加1鲁沥,直至position到達limit處為止睡扬。因此,在寫入數(shù)據(jù)后黍析,開始讀數(shù)據(jù)前,需要設置position和limit的值屎开,以便get方法能夠正確讀入前面寫入的元素阐枣。
這個設置應該是讓limit=position,然后position=0奄抽,為了方便蔼两,Buffer類提供了一個方法flip(),來完成這個設置逞度。其代碼如下:

/**
     * 測試flip操作额划,flip就是從寫入轉(zhuǎn)為讀出前的一個設置buffer屬性的操作,其意義是將limit=position档泽,position=0
     */
    private static void testFlip() {
       CharBuffer buffer = CharBuffer.allocate(10);
       buffer.put("abc");
       buffer.flip();
       char[] chars = new char[buffer.remaining()];
       buffer.get(chars);
       System.out.println(chars);

        //以下操作與flip等同
       buffer.clear();
       buffer.put("abc");
       buffer.limit(buffer.position());
       buffer.position(0);
       chars = new char[buffer.remaining()];
       buffer.get(chars);
        System.out.println(chars);
}

1.5compact壓縮
壓縮compact()方法是為了將讀取了一部分的buffer俊戳,其剩下的部分整體挪動到buffer的頭部(即從0開始的一段位置),便于后續(xù)的寫入或者讀取馆匿。其含義為limit=limit-position抑胎,position=0,測試代碼如下:

 private static void testCompact() {
       CharBuffer buffer = CharBuffer.allocate(10);
       buffer.put("abcde");
       buffer.flip();
        //先讀取兩個字符
       buffer.get();
       buffer.get();
       showBuffer(buffer);
        //壓縮
       buffer.compact();
        //繼續(xù)寫入
       buffer.put("fghi");
       buffer.flip();
        showBuffer(buffer);
        //從頭讀取后續(xù)的字符
       char[] chars = new char[buffer.remaining()];
       buffer.get(chars);
       System.out.println(chars);
}

1.6duplicate復制
復制緩沖區(qū)渐北,兩個緩沖區(qū)對象實際上指向了同一個內(nèi)部數(shù)組阿逃,但分別管理各自的屬性。

    private static void testDuplicate() {
        CharBuffer buffer = CharBuffer.allocate(10);
       buffer.put("abcde");
       CharBuffer buffer1 = buffer.duplicate();
       buffer1.clear();
       buffer1.put("alex");
       showBuffer(buffer);
       showBuffer(buffer1);
}

1.7 slice緩沖區(qū)切片

緩沖區(qū)切片,將一個大緩沖區(qū)的一部分切出來恃锉,作為一個單獨的緩沖區(qū)搀菩,但是它們公用同一個內(nèi)部數(shù)組。切片從原緩沖區(qū)的position位置開始破托,至limit為止肪跋。原緩沖區(qū)和切片各自擁有自己的屬性,測試代碼如下:

  /**
 * slice Buffer 和原有Buffer共享相同的底層數(shù)組
 */
public class NioTest6 {

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        for (int i=0;i<buffer.capacity();i++){
            buffer.put((byte)i);
        }
        buffer.position(2);
        buffer.limit(6);

        ByteBuffer sliceBuffer = buffer.slice();
        for (int i=0;i < sliceBuffer.capacity();i++){
            byte b = sliceBuffer.get(i);
            b *= 2;
            sliceBuffer.put(i,b);
        }

        buffer.position(0);
        buffer.limit(buffer.capacity());

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

1.8只讀Buffer
我們可以隨時將一個普通Buffer調(diào)用asReadOnlyBuffer方法返回一個只讀Buffer炼团,但不能將一個只讀Buffer轉(zhuǎn)換成讀寫B(tài)uffer

public class NioTest7 {

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        System.out.println(buffer.getClass());
        for (int i=0;i<buffer.capacity();i++){
            buffer.put((byte)i);
        }

        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
        System.out.println(readOnlyBuffer.getClass());

        //只讀buffer澎嚣,不可以寫
//        readOnlyBuffer.position(0);
//        readOnlyBuffer.put((byte)2);
    }
}
  1. 字節(jié)緩沖區(qū)
    為了便于示例,前面的例子都使用了CharBuffer緩沖區(qū)瘟芝,但實際上應用最廣易桃,使用頻率最高,也是最重要的緩沖區(qū)是字節(jié)緩沖區(qū)ByteBuffer锌俱。因為ByteBuffer中直接存儲字節(jié)晤郑,所以在不同的操作系統(tǒng)、硬件平臺贸宏、文件系統(tǒng)和JDK之間傳遞數(shù)據(jù)時不涉及編碼造寝、解碼和亂碼問題,也不涉及Big-Endian和Little-Endian大小端問題吭练,所以它是使用最為便利的一種緩沖區(qū)诫龙。

2.1視圖緩沖區(qū)
ByteBuffer中存儲的是字節(jié),有時為了方便鲫咽,可以使用asCharBuffer()等方法將ByteBuffer轉(zhuǎn)換為存儲某基本類型的視圖签赃,例如CharBuffer、IntBuffer分尸、DoubleBuffer锦聊、ShortBuffer、LongBuffer和FloatBuffer箩绍。
如此轉(zhuǎn)換后孔庭,這兩個緩沖區(qū)共享同一個內(nèi)部數(shù)組,但是對數(shù)組內(nèi)元素的視角不同材蛛。以CharBuffer和ByteBuffer為例圆到,ByteBuffer將其視為一個個的字節(jié)(1個字節(jié)),而CharBuffer則將其視為一個個的字符(2個字節(jié))卑吭。若此ByteBuffer的capacity為12构资,則對應的CharBuffer的capacity為12/2=6。與duplicate創(chuàng)建的復制緩沖區(qū)類似陨簇,該CharBuffer和ByteBuffer也各自管理自己的緩沖區(qū)屬性吐绵。
還有一點需要注意的是迹淌,在創(chuàng)建視圖緩沖區(qū)的時候ByteBuffer的position屬性的取值很重要,視圖會以當前position的值為開頭己单,以limit為結(jié)尾唉窃。例子如下:

private static void testElementView() {
    ByteBuffer buffer =ByteBuffer.allocate(12);
    //存入四個字節(jié),0x00000042
    buffer.put((byte) 0x00).put((byte)0x00).put((byte) 0x00).put((byte) 0x42);
    buffer.position(0);
    //轉(zhuǎn)換為IntBuffer,并取出一個int(四個字節(jié))
    IntBuffer intBuffer =buffer.asIntBuffer();
    int i =intBuffer.get();
   System.out.println(Integer.toHexString(i));
}

不同元素需要的字節(jié)數(shù)不同:char為2字節(jié)纹笼,short為2字節(jié)纹份,int為4字節(jié),float為4字節(jié)廷痘,long為8字節(jié)蔓涧,double也是8字節(jié)。

2.2存取數(shù)據(jù)元素
也可以不通過視圖緩沖區(qū)笋额,直接向ByteBuffer中存入和取出不同類型的元素元暴,其方法名為putChar()或者getChar()之類。例子如下:

private static void testPutAndGetElement() {
    ByteBuffer buffer =ByteBuffer.allocate(12);
    //直接存入一個int
    buffer.putInt(0x1234abcd);
    //以byte分別取出
    buffer.position(0);
    byte b1 = buffer.get();
    byte b2 = buffer.get();
    byte b3 = buffer.get();
    byte b4 = buffer.get();
   System.out.println(Integer.toHexString(b1&0xff));
   System.out.println(Integer.toHexString(b2&0xff));
   System.out.println(Integer.toHexString(b3&0xff));
   System.out.println(Integer.toHexString(b4&0xff));
}

2.3 字節(jié)序

終于又要講到字節(jié)序了兄猩,詳細參見https://zhuanlan.zhihu.com/p/25435644茉盏。

簡單說來,當某個元素(char枢冤、int鸠姨、double)的長度超過了1個字節(jié)時,則由于種種歷史原因淹真,它在內(nèi)存中的存儲方式有兩種讶迁,一種是Big-Endian,一種是Little-Endian核蘸。
Big-Endian就是高位字節(jié)排放在內(nèi)存的低地址端巍糯,低位字節(jié)排放在內(nèi)存的高地址端。 簡單來說值纱,就是我們?nèi)祟愂煜さ拇娣欧绞健?br> Little-Endian就是低位字節(jié)排放在內(nèi)存的低地址端,高位字節(jié)排放在內(nèi)存的高地址端坯汤。
Java默認是使用Big-Endian的虐唠,因此上面的代碼都是以這種方式來存放元素的。但是惰聂,其他的一些硬件(CPU)疆偿、操作系統(tǒng)或者語言可能是以Little-Endian的方式來存儲元素的。因此NIO提供了相應的API來支持緩沖區(qū)設置為不同的字節(jié)序搓幌,其方法很簡單杆故,代碼如下:

    privatestatic void testByteOrder() {
        ByteBuffer buffer =ByteBuffer.allocate(12);
        //直接存入一個int
       buffer.putInt(0x1234abcd);
       buffer.position(0);
        intbig_endian= buffer.getInt();
       System.out.println(Integer.toHexString(big_endian));
       buffer.rewind();
        intlittle_endian=buffer.order(ByteOrder.LITTLE_ENDIAN).getInt();
       System.out.println(Integer.toHexString(little_endian));
    }

輸出為:
1234abcd
cdab3412
使用order方法可以隨時設置buffer的字節(jié)序,其參數(shù)取值為ByteOrder.LITTLE_ENDIAN以及ByteOrder.BIG_ENDIAN溉愁。

2.4直接緩沖區(qū) DirectByteBuffer
最后一個需要掌握的概念是直接緩沖區(qū)处铛,它是以創(chuàng)建時的開銷換取了IO時的高效率。另外一點是,直接緩沖區(qū)使用的內(nèi)存是直接調(diào)用了操作系統(tǒng)api分配的撤蟆,繞過了JVM堆棧奕塑。
直接緩沖區(qū)通過ByteBuffer.allocateDirect()方法創(chuàng)建,并可以調(diào)用isDirect()來查詢一個緩沖區(qū)是否為直接緩沖區(qū)家肯。
一般來說龄砰,直接緩沖區(qū)是最好的IO選擇。

public class NioTest8 {

    public static void main(String[] args) throws IOException {
        FileInputStream inputStream = new FileInputStream("input2.txt");
        FileOutputStream outputStream = new FileOutputStream("output2.txt");

        FileChannel inputChannel = inputStream.getChannel();
        FileChannel outputChannel = outputStream.getChannel();

        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

        while(true){
            //每一次讀取之前都將buffer狀態(tài)初始化
            buffer.clear();
            int read = inputChannel.read(buffer);

            System.out.println("read:" + read);

            if(-1 == read){
                break;
            }
            buffer.flip();
            outputChannel.write(buffer);
        }

        inputChannel.close();
        outputChannel.close();
    }
}

2.5讨衣、MappedByteBuffer 內(nèi)存映射文件
文件的內(nèi)容直接映射到內(nèi)存里面换棚,在內(nèi)存中任何的信息修改,最終都會被寫入到磁盤文件中反镇,即MappedByteBuffer是一種允許java程序直接從內(nèi)存訪問的特殊的文件固蚤,可以將整個文件或整個文件的一部分映射到內(nèi)存中,由操作系統(tǒng)負責將頁面請求的內(nèi)存數(shù)據(jù)修改寫入到文件中愿险。應用程序只需要處理內(nèi)存的數(shù)據(jù)颇蜡,這樣可以實現(xiàn)迅速的IO操作,用于內(nèi)存映射文件的這個內(nèi)存是堆外內(nèi)存

public class NioTest9 {

    public static void main(String[] args) throws IOException {
        RandomAccessFile randomAccessFile = new RandomAccessFile("NioTest9.txt","rw");
        FileChannel fileChannel = randomAccessFile.getChannel();

        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 6);

        mappedByteBuffer.put(0, (byte) 'a');
        mappedByteBuffer.put(4, (byte) 'b');

        randomAccessFile.close();
    }
}
  1. 小結(jié)
    與Stream相比辆亏,Buffer引入了更多的概念和復雜性风秤,這一切的努力都是為了實現(xiàn)NIO的經(jīng)典編程模式,即用一個線程來控制多路IO扮叨,從而極大的提高服務器端IO效率缤弦。Buffer、Channel和Selector共同實現(xiàn)了NIO的編程模式彻磁,其中Buffer也可以被獨立的使用碍沐,用來完成緩沖區(qū)的功能。

參考:
Java NIO編程實例之一Buffer

Java NIO通俗編程之緩沖區(qū)內(nèi)部細節(jié)狀態(tài)變量position,limit,capacity(二)

Java NIO:Buffer衷蜓、Channel 和 Selector

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末累提,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子磁浇,更是在濱河造成了極大的恐慌斋陪,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡眶诈,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門友题,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人戴质,你說我怎么就攤上這事度宦√呦唬” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵斗埂,是天一觀的道長符糊。 經(jīng)常有香客問我,道長呛凶,這世上最難降的妖魔是什么男娄? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮漾稀,結(jié)果婚禮上模闲,老公的妹妹穿的比我還像新娘。我一直安慰自己崭捍,他們只是感情好尸折,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著殷蛇,像睡著了一般实夹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上粒梦,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天亮航,我揣著相機與錄音,去河邊找鬼匀们。 笑死缴淋,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的泄朴。 我是一名探鬼主播重抖,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼祖灰!你這毒婦竟也來了钟沛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤局扶,失蹤者是張志新(化名)和其女友劉穎恨统,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體详民,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡延欠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年陌兑,在試婚紗的時候發(fā)現(xiàn)自己被綠了沈跨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡兔综,死狀恐怖饿凛,靈堂內(nèi)的尸體忽然破棺而出狞玛,到底是詐尸還是另有隱情,我是刑警寧澤涧窒,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布心肪,位于F島的核電站,受9級特大地震影響纠吴,放射性物質(zhì)發(fā)生泄漏硬鞍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一戴已、第九天 我趴在偏房一處隱蔽的房頂上張望固该。 院中可真熱鬧,春花似錦糖儡、人聲如沸伐坏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽桦沉。三九已至,卻和暖如春金闽,著一層夾襖步出監(jiān)牢的瞬間纯露,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工呐矾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留苔埋,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓蜒犯,卻偏偏與公主長得像组橄,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子罚随,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容