NIO流—理解Buffer、Channel概念和NIO的讀寫操作


NIO流與IO流的區別

面向流與面向塊
  • IO流是每次處理一個或多個字節,效率很慢(字符流處理的也是字節,只是對字節進行編碼和解碼處理)。
  • NIO流是以數據塊為單位來處理,緩沖區就是用於讀寫的數據塊。緩沖區的IO操作是由底層操作系統實現的,效率很快。
阻塞式與非阻塞式
  • IO流是阻塞式的,使用read()與write()方法時,執行期間只能等待該方法完成。
  • NIO流是非阻塞式的,執行讀寫時依然可以做別的事情,不會阻塞線程,提高資源利用率,NIO流的Selector就是非阻塞式的。
NIO加入了Selector(選擇器)
  • Selector可以讓一個線程監視多個Channel,只需要一個線程處理所有管道,減少線程開銷。

nio流的相關類都放在java.nio包中,大體如下:

  • java.nio 包:包含各種類型的Buffer(緩沖區)
  • java.nio.channels包:包含各種Channel(管道) 和Selector(選擇器)
  • java.nio.charset包:包含各種處理字符集的類 

Buffer(緩沖區)

  • 常用ByteBuffer 和 CharBuffer,還有一系列值類型Buffer,可以應用於不同類型。
  • 所有Buffer都是抽象類,無法直接實例化。創建緩沖區要調用XxxBuffer allocate(),參數是緩沖區容量
  • 緩沖區數據存放在內存中,讀寫效率高。緩沖區有記錄指針,能改變讀寫的起始點,根據不同需求,靈活處理數據。

Buffer參數說明

  • capacity(容量):緩沖區支持的最大容量。
  • position(記錄指針位置):是緩沖區讀寫數據的起始點,初始值為0。position隨着數據的加入而改變,例如讀取2個數據到Buffer中,則position = 2。
  • limit(界限):是緩沖區讀寫數據的終止點,limit之后的區域無法訪問。
  • mark(標記):mark在0~position之間,設置該值就會把position移動到mark處。

Buffer的常用方法:

  • flip():確定緩沖區數據的起始點和終止點,為輸出數據做准備(即寫入通道)。此時:limit = position,position = 0。
  • clear():緩沖區初始化,准備再次接收新數據到緩沖區。position = 0,limit = capacity。
  • hasRemaining():判斷postion到limit之間是否還有元素。
  • rewind():postion設為0,則mark值無效。
  • limit(int newLt):設置界限值,並返回一個緩沖區,該緩沖區的界限和limit()設置的一樣。
  • get()和put():獲取元素和存放元素。使用clear()之后,無法直接使用get()獲取元素,需要使用get(int index)根據索引值來獲取相應元素。

圖片理解Buffer讀寫數據的流程變化

     

 

Channel(通道)

Channel通過Buffer(緩沖區)進行讀寫操作。read()表示讀取通道數據到緩沖區,write()表示把緩沖區數據寫入到通道。

Channel需要節點流作為創建基礎,例如FileInputStream和FileOutputStream()的getChannel()。RandomAccessFile也能創建文件通道,支持讀寫模式。通過IO流創建的通道是單向的,使用RandomAccessFile創建的通道支持雙向。

通道可以異步讀寫,異步讀寫表示通道執行讀寫操作時,也能做別的事情,解決線程阻塞。如果使用文件管道(FileChannel),建議用RandomAccessFile來創建管道,因為該類支持讀寫模式以及有大量處理文件的方法。

Channel實現類
FileChannel //讀寫文件通道
SocketChannel //通過TCP讀寫網絡數據通道
ServerSocketChannel //監聽多個TCP連接
DataChannel //通過UDP讀寫網絡數據通道
Pipe.SinkChannel、Pipe.SourceChannel //線程通信管道傳輸數據
Channel常用方法
read()  //從Buffer中讀取數據。
write() //寫入數據到Buffer中。
map()   //把管道中部分數據或者全部數據映射成MappedByteBuffer,本質也是一個ByteBuffer。map()方法參數(讀寫模式,映射起始位置,數據長度)。
force() //強制將此通道的元數據也寫入包含該文件的存儲設備。

charset(字符集)

  • 包含了字節和 Unicode 字符之間轉換的 charset,還定義了用於創建解碼器和編碼器以及獲取與 charset 關聯的各種方法。
  • CharsetDecoder(解碼器):把字節轉成字符,例如查看文本數據,需要轉成字符才能查看,如果是字節,就看不懂了。
  • CharsetEncoder(編碼器):把字符轉成字節,才能被計算機理解。 因為字節是計算機最小的存儲單位,所以Channel的IO操作都與ByteBuffer有關。
  • 解碼器和編碼器都不能直接創建,需要一個Charset對象來創建對應的解碼器和編碼器。
Charset的常用方法
forName():根據傳入的字符集獲得對應的字符集對象。
defaultCharset():獲得當前使用的默認字符集。
availableCharsets():獲得所有有效的字符集。
當使用nio來獲取文件內容時,如果是文本數據,那么需要進行轉碼,才能查看正確內容,這就需要解碼器。 如果要把字符數據寫入文件,需要將CharBuffer轉碼成ByteBuffer,這就需要編碼器。

 

Buffer實例

        //創建字節緩沖區,容量1024
        ByteBuffer buff = ByteBuffer.allocate(1024);
        System.out.println(buff.position());//讀寫起始點
        System.out.println(buff.limit());//界限位置
        //字節緩沖區放入3個int數值
        buff.putInt(10);
        buff.putInt(15);
        buff.putInt(20);
        System.out.println(buff.position());// 放入數據后,position:12
        //切換讀取模式,方便輸出數據
        buff.flip();
        System.out.println("切換寫模式后,position———limit," + buff.position() + "———" + buff.limit());
        //切換寫入模式,方便獲取數據
        buff.clear();
        System.out.println("切換讀模式后,position———limit," + buff.position() + "———" + buff.limit());
上面代碼中,Buffer的allocate(int capacity)用於創建緩沖區。position()與limit()分別獲取當前緩沖區的讀寫點與終止點。使用put()放入數據,put()有一系列方法放入不同數類型據。因為是字節緩沖區,而int數值是占4字節,所以position是12。flip()和clear()使用之后,會對讀寫點和終止點進行改變。當然,還有一系列方法,可自己實驗。

創建Channel

        File f1 = new File("D:\\reviewIO\\ChannelDemo.txt");
        File f2 = new File("D:\\reviewIO\\ChannelDemo2.txt");

        //文件輸入/輸出流來創建通道
        FileInputStream fis = new FileInputStream(f1);
        FileOutputStream fos = new FileOutputStream(f2);
        FileChannel inChannel = fis.getChannel();//讀取通道
        FileChannel outChannel = fos.getChannel();//寫入通道
        //使用RandomAccessFile來創建FileChannel
        FileChannel rafChannel = new RandomAccessFile(f1,"rw").getChannel();
        FileChannel rafChannel2 = new RandomAccessFile(f2,"rw").getChannel();
上面代碼中,使用輸入/輸出流來分別創建了文件通道,雖然通道是雙向的。但輸入流的通道只能用於讀取數據到緩沖區,輸出流的通道用於把緩沖區數據寫入通道。使用RandomAccessFile類也可以創建通道,RandomAccessFile可以設為讀寫模式。

使用Channel的內存映射

        File f1 = new File("D:\\reviewIO\\ChannelDemoF1.txt");
        File f2 = new File("D:\\reviewIO\\ChannelDemoF2.txt");
        //使用RandomAccessFile來創建FileChannel
        FileChannel inChannel = new RandomAccessFile(f1,"rw").getChannel();
        FileChannel outChannel = new RandomAccessFile(f2,"rw").getChannel();
        
        //把rafChannel通道的全部數據映射成ByteBuffer
        MappedByteBuffer mapBuff = inChannel.map(MapMode.READ_ONLY, 0, f1.length());
上面代碼中,Channel直接把通道中全部數據映射成ByteBuffer,使用內存映射可以大幅度提高文件拷貝性能,后面會進行比較。也可以使用Buffer的allocate()來自由分配緩沖區容量,看個人需求來。

使用NIO讀取文件數據

        File file = new File("D:\\reviewIO\\word.txt");
        //以只讀模式來創建通道
        FileChannel inChannel = new RandomAccessFile(file,"r").getChannel();
        //創建字節緩沖區
        ByteBuffer bytebuf = ByteBuffer.allocate(1024);    
        //默認字符集創建解碼器
        CharsetDecoder decoder = Charset.defaultCharset().newDecoder();
        
        while((inChannel.read(bytebuf)) != -1) {//讀取通道數據到緩沖區中,非-1就代表有數據
            //確定緩沖區數據的起點和終點
            bytebuf.flip();
            //對bytebuf進行解碼,避免亂碼
            CharBuffer decode = decoder.decode(bytebuf);
            System.out.println(decode.toString());
            //清空緩沖區,再次放入數據
            bytebuf.clear();
        }    
使用nio流讀取文件要注意flip()和clear()方法,flip()是確定緩沖區數據的起點和終點位置,避免訪問到limit()之后的區域。clear()是把緩沖區初始化,方便再次寫入數據到緩沖區。因為是ByteBuffer,所以無法直接顯示文本內容,需要使用解碼器來轉碼成字符緩沖區。

nio流寫入字符串到文件中

        Scanner sc = new Scanner(System.in);
        File file = new File("D:\\reviewIO\\writeNio.java");
        if(!file.exists()) 
            file.createNewFile();
        //RandomAccessFile創建通道,是讀寫模式
        FileChannel inChannel = new RandomAccessFile(file,"rw").getChannel();
        //創建字符緩沖區,容量1024
        CharBuffer charBuf = CharBuffer.allocate(1024);    
        //使用默認字符集創建編碼器
        CharsetEncoder encoder = Charset.defaultCharset().newEncoder();
        String str = null;
        while(!(str = sc.next()).equals("end")) {
            //往緩沖區中寫入數據
            charBuf.put(str+"\r\n");
            //為輸出緩沖區數據做准備
            charBuf.flip();
            //對緩沖區進行編碼,避免亂碼
            ByteBuffer bb = encoder.encode(charBuf);
            //把緩沖區數據寫入通道
            inChannel.write(bb);
            //Buffer初始化,為下一次讀取數據做准備
            charBuf.clear();
        }
上面代碼實現從鍵盤輸入數據,然后寫入指定文件中。因為緩沖區是字符類型,而通道的讀寫操作都是使用byteBuffer,所以要使用編碼器將CharBuffer轉碼成ByteBuffer,才能往通道中寫入數據。

使用內存映射拷貝文件

    public static void copyLargeFile(String srcPath, String destPath) throws IOException {
        File src = new File(srcPath);//源文件
        File dest = new File(destPath);//拷貝的文件
        FileChannel fcin = null;//文件輸入通道
        FileChannel fcout = null;//文件輸出通道
        if(!src.isFile()) {
             System.err.println("源路徑指向的不是文件");
             return;
        }
        if(!src.exists() || !dest.exists()) {
            System.err.println("源文件或者拷貝文件路徑不存在,請檢查!");
            return;
        }
        fcin = new FileInputStream(src).getChannel();
        fcout = new FileOutputStream(dest).getChannel();
        //把文件輸入通道數據全部映射成ByteBuffer
        MappedByteBuffer buf = fcin.map(FileChannel.MapMode.READ_ONLY, 0, fcin.size());                
        //fcout寫入數據,數據源是buf緩沖區
        fcout.write(buf);
        //buf初始化,准備再次接收數據
        buf.clear();
    }
NIO拷貝大型文件性能很好,如果是拷貝小文件那和IO流差別不大。所以那個體系用得爽就用那個。

自定義緩沖區分批次拷貝文件

    public static void copyLargeFile2(String srcPath, String destPath) throws IOException {
        File src = new File(srcPath);//源文件
        File dest = new File(destPath);//拷貝文件路徑
        FileChannel fcin = null;//文件輸入通道
        FileChannel fcout = null;//文件輸出通道
        if(!src.isFile()) {
             System.err.println("源路徑指向的不是文件");
             return;
        }
        if(!src.exists() || !dest.exists()) {
            System.err.println("源文件或者拷貝文件路徑不存在,請檢查!");
            return;
        }
        fcin = new FileInputStream(src).getChannel();
        fcout = new FileOutputStream(dest).getChannel();
        //容量1024緩沖區
        ByteBuffer buf = ByteBuffer.allocate(1024);
        while((fcin.read(buf)) != -1) {//讀取管道數據到緩沖區中,為1則結束
            //確定緩沖區數據的起點和終點
            buf.flip();
            //fcout寫入數據,數據源是buf緩沖區
            fcout.write(buf);
            //buf初始化,准備再次接收數據
            buf.clear();
        }        
    }

使用自定義緩沖區可以避免一次性寫入大量數據到內存中。還有如果拷貝的是文本數據,也建議使用ByteBuffer。不要使用CharBuffer作為緩沖區,省略編碼步驟。如果是讀取文本數據並且顯示的話還是需要解碼器轉碼成字符緩沖區。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM