Java I/O回顧

最近在學習netty,其中有對比bio、nio、netty使用上的不同,也趁此機會回顧了相關知識,加深下理解,主要涉及的有FileInputStream、FileOutputStream、BufferedInputStream、BufferedOutputStream、FileChannel、MappedByteBuffer等知識點。

一、FileIn(Out)putStream(單字節(jié))

public static void fileCopy(String src,String dest) throws IOException{
    long t = System.currentTimeMillis();
    File file = new File(src);
    try(
        FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(new File(dest));
    ){
        int result = 0;
        while((result = fis.read()) != -1){
            fos.write(result);
        }
    }
    System.err.println("file copy use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
}

控制臺輸出:
file copy use time=62065 file.size=20MB
從輸出接口可以看到,20MB文件copy用時一分鐘多,可見不使用緩沖區(qū),單字節(jié)讀寫時執(zhí)行慢的無法接受。

這里使用了try-with-resources的方式來進行流的自動關閉,對該知識點不熟悉的可參考Java 7中的Try-with-resources;

二、FileIn(Out)putStream(字節(jié)數組)

public static void fileCopyWithBuffer(String src,String dest) throws IOException{
    long t = System.currentTimeMillis();
    File file = new File(src);
    try(
        FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(new File(dest));
    ){
        byte[] buffer = new byte[1024];
        int byteRead = 0;
        while((byteRead = fis.read(buffer)) != -1){
            fos.write(buffer,0,byteRead);
        }
    }
    System.err.println("fileCopyWithBuffer use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
}

控制臺輸出:
fileCopyWithBuffer use time=140 file.size=20MB
可見使用buffer進行批量讀寫后,性能有了質的提高.

下面我們來調整buffer數組的長度為10240,看下輸出:
fileCopyWithBuffer use time=45 file.size=20MB
經過多次嘗試,發(fā)現適當的增加buffer的長度,可以明顯提升處理速度。

三、BufferedIn(Out)putStream(單字節(jié))

public static void fileCopyWithBufferStream(String src,String dest) throws IOException{
    long t = System.currentTimeMillis();
    File file = new File(src);
    try(
        FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(new File(dest));
        
        BufferedInputStream bis = new BufferedInputStream(fis);
        BufferedOutputStream bos = new BufferedOutputStream(fos);
    ){
        int result = 0;
        while((result =bis.read()) != -1){
            bos.write(result);
        }
    }
    System.err.println("fileCopyWithBufferStream use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
}

控制臺輸出:
fileCopyWithBufferStream use time=1199 file.size=20MB
相對于單字節(jié)的流讀寫方式,處理時間有62s提升至1.2s,提升還是很大的。

BufferedInputStream創(chuàng)建時內部會默認創(chuàng)建一個長度8192的byte數組,read方法實際上是在這個內存數組里面讀取數據,下面我們看下read方法的源碼:

public synchronized int read() throws IOException {
    if (pos >= count) { // 當數據已讀完時
        fill(); // 調用上層inputStream內read方法讀取數據填充buf
        if (pos >= count) // 如果仍無可讀數據,說明數據已讀完
            return -1;
    }
    //從此處可看到read操作實際讀取的是buf數組的數據
    //& 0xff的目的是消除后八位之前的位數據,保證讀出的字節(jié)數據無變化,防止首位為1的情況轉為int時高位為填充為1
    return getBufIfOpen()[pos++] & 0xff; 
}
  • 當緩存區(qū)無可讀數據時,調用fill方法,fill方法內部會考慮mark和reset的邏輯,設置新讀入數據再數組內的存放位置
  • 調用InputStream內的read(byte b[], int off, int len)方法進行數據的“批量”讀??;
  • 調用bis.read()時,從buffer內返回數據

“批量”的讀數據,從而提高了整體的執(zhí)行速度。fill()方法的實現此處不再展開,可參考BufferedInputStream源碼分析.

BufferedOutputStream的write方法,也是先將數據存入buf,當buf存滿時,將數據刷出;

public synchronized void write(int b) throws IOException {
    if (count >= buf.length) {
        flushBuffer();
    }
    buf[count++] = (byte)b;
}

四、BufferedIn(Out)putStream(多字節(jié))

public static void fileCopyWithBufferStreamAndBuffer(String src,String dest) throws IOException{
    long t = System.currentTimeMillis();
    File file = new File(src);
    try(
        FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(new File(dest));
        
        BufferedInputStream bis = new BufferedInputStream(fis);
        BufferedOutputStream bos = new BufferedOutputStream(fos);
    ){
        byte[] buffer = new byte[1024];
        int byteRead = 0;
        while((byteRead = bis.read(buffer)) != -1){
            bos.write(buffer,0,byteRead);// 1
            //bos.write(buffer); // 2
            // 1和2的區(qū)別是,最后一次讀取的數據可能不能放滿buffer,那樣上一次留存的數據就會
            // 被一起寫出,可以通過定義一個15長度的byte數組,寫出時使用長度10的byte數組,就能看到結果了
        }
    }
    System.err.println("fileCopyWithBufferStreamAndBuffer use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
}

控制臺輸出:
fileCopyWithBufferStreamAndBuffer use time=99 file.size=20MB
可見使用buffer進行批量讀寫后,性能有了質的提高.

下面我們來調整buffer數組的長度為10240,看下輸出:
fileCopyWithBufferStreamAndBuffer use time=46 file.size=20MB
經過多次嘗試,發(fā)現適當的增加buffer的長度,可以明顯提升處理速度。

五、FileChannel

public static void fileCopyWithChannel(String src,String dest) throws IOException{
    long t = System.currentTimeMillis();
    File file = new File(src);
    try(
        FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(new File(dest));
        
        FileChannel in = fis.getChannel();
        FileChannel out = fos.getChannel();
    ){
        in.transferTo(0, in.size(), out);
    }
    System.err.println("fileCopyWithChannel use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
}

控制臺輸出:
fileCopyWithChannel use time=85 file.size=20MB

從輸出結果看,單純的copy文件上,使用Channel并無明顯的性能優(yōu)勢。
ps:transferTo方法比較耗費內存資源,建議只在小文件或小使用量時使用,避免出現資源不足等異常。

NIO的關鍵點是通道和緩沖區(qū),這里不再過多展開,NIO入門推薦Java NIO 系列教程。

六、FileChannel(ByteBuffer)

public static void nioBufferCopy(String src,String dest) throws IOException {  
    long t = System.currentTimeMillis();
    File file = new File(src);
    try(
            FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(new File(dest));
        
        FileChannel in = fis.getChannel();
        FileChannel out = fos.getChannel();
        ){
        ByteBuffer buffer = ByteBuffer.allocate(4096);  
        while (in.read(buffer) != -1) {  
            buffer.flip();  
            out.write(buffer);  
            buffer.clear();  
        }  
    } 
    
    System.err.println("nioBufferCopy use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
}  

控制臺輸出:
nioBufferCopy use time=182 file.size=20MB

下面我們來調整buffer數組的長度為40960,看下輸出:
nioBufferCopy use time=60 file.size=20MB

七、FileChannel(MappedByteBuffer)

public static void nioMappedByteBufferCopy(String src,String dest) throws IOException {  
    long t = System.currentTimeMillis();
    File file = new File(src);
    try(
            FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(new File(dest));
        
        FileChannel in = fis.getChannel();
        FileChannel out = fos.getChannel();
        ){  
        MappedByteBuffer  mappedByteBuffer = in.map(MapMode.READ_ONLY, 0, in.size());
        out.write(mappedByteBuffer);  
    } 
    
    System.err.println("nioMappedByteBufferCopy use time="+(System.currentTimeMillis()-t)+" file.size="+(file.length()/1024/1024)+"MB");
} 

控制臺輸出:
nioMappedByteBufferCopy use time=50 file.size=20MB
使用內存映射文件的方式速度還是比較理想的,更多內容推薦閱讀深入淺出MappedByteBuffer,另外,推薦關注占小狼,他寫的精品文章很多!

八、Buffered(Reader)Writer

字符流的使用相對簡單,這里給出一個示例代碼。

public static void bufferReaderWriterTest() throws IOException{
    List<String> list = new ArrayList<String>(20);
    for(int i=0;i<20;i++){
        list.add(""+i);
    }
    File file = new File("brwtest.txt");
    try(
            
        BufferedWriter bw = new BufferedWriter(new FileWriter(file));
        BufferedReader br = new BufferedReader(new FileReader(file));
    ){
        for (String item : list) {
            bw.write(item);
            bw.newLine();
        }
        //此處不調用flush方法,下邊讀取時內容會為空
        bw.flush();
        
        String line = null;
        while ((line = br.readLine()) != null) {
            System.err.println(line);
        }
        
        //org.apache.commons.io.IOUtils,有許多工具包內提供了IO操作方法,可以優(yōu)先考慮使用
//          List<String> lines = IOUtils.readLines(br);
//          for (String string : lines) {
//              System.err.println(string);
//          }
    }
}

小結

我個人寫這篇文章或者說寫這些demo代碼的收獲有:

  1. 自以為很熟悉的I/O類,動起手來才發(fā)現自己離“順手拈來”還有一段不小的差距;
  2. 從源碼層面了解不同流的實現方式,更深一層的提高了自己對java I/O知識的理解;
  3. 發(fā)現了過往所學知識的弱點,看別人博客進行學習,大致知其然,未系統(tǒng)梳理,未動手練習,未理解其實現方式和原理,浮于表面,隨著時間的流逝,這些知識成了零散模糊的片段

I/O是java中基礎的知識點,你不妨也閉起眼思考下你能使用多少種不同的方式實現文件復制,或許你會有不同的發(fā)現!

ps:本機mac air,測試代碼有其隨意性,copy速度僅能體現量級上的趨勢,以供參考。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容