面試官:Java中緩衝流真的效能很好嗎?我看未必

JavaBuild發表於2024-06-17

一、寫在開頭

上一篇文章中,我們介紹了Java IO流中的4個基類:InputStream、OutputStream、Reader、Writer,那麼這一篇中,我們將以四個基類所衍生出來,應對不同場景的資料流進行學習。
image

二、衍生資料流分類

我們上面說了java.io包中有40多個類,都從InputStream、OutputStream、Reader、Writer這4個類中衍生而來,我們以操作物件的維度進行如下的區分:

2.1 檔案流

檔案流也就是直接操作檔案的流,可以細分為位元組流(FileInputStream 和 FileOuputStream)和字元流(FileReader 和 FileWriter),我們在上面的已經說了很多了,這裡就再贅述啦。

2.2 陣列流

所謂陣列流就是將記憶體中有限的資料進行讀寫操作的流,適應於資料量小,無需利用檔案儲存,提升程式效率。

我們以ByteArrayInputStream(位元組陣列輸入流)為例:

public class TestService{
    public static void main(String[] args)  {
        try {
            ByteArrayInputStream bi = new ByteArrayInputStream("JavaBuild".getBytes());
            int content;
            while ((content = bi.read()) != -1) {
                System.out.print((char) content);
            }
            // 關閉輸入流,釋放資源
            bi.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

位元組陣列輸出流(ByteArrayOutputStream)亦是如此,它們不需要建立臨時檔案,直接在記憶體中就可以完成對位元組陣列的壓縮,加密,讀寫以及序列化。

2.3 管道流

管道(Pipe)作為一種在計算機內通訊的媒介,無論是在作業系統(Unix/Linux)層面還是JVM層面都至關重要,我們今天提到的通道流就是在JVM層面,同一個程序中不同執行緒之間資料互動的載體。

我們以PipedOutputStream和PipedInputStream為例,透過PipedOutputStream將一串字元寫入到記憶體中,再透過PipedInputStream讀取輸出到控制檯,整個過程並沒有臨時檔案的事情,資料僅在兩個執行緒之間流轉。

public class TestService{
    public static void main(String[] args) throws IOException {
        // 建立一個 PipedOutputStream 物件和一個 PipedInputStream 物件
        final PipedOutputStream pipedOutputStream = new PipedOutputStream();
        final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream);

        // 建立一個執行緒,向 PipedOutputStream 中寫入資料
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 將字串 "沉默王二" 轉換為位元組陣列,並寫入到 PipedOutputStream 中
                    pipedOutputStream.write("My name is JavaBuild".getBytes());
                    // 關閉 PipedOutputStream,釋放資源
                    pipedOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
        // 建立一個執行緒,從 PipedInputStream 中讀取資料並輸出到控制檯
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 定義一個位元組陣列用於儲存讀取到的資料
                    byte[] flush = new byte[1024];
                    // 定義一個變數用於儲存每次讀取到的位元組數
                    int len = 0;
                    // 迴圈讀取位元組陣列中的資料,並輸出到控制檯
                    while (-1 != (len = pipedInputStream.read(flush))) {
                        // 將讀取到的位元組轉換為對應的字串,並輸出到控制檯
                        System.out.println(new String(flush, 0, len));
                    }
                    // 關閉 PipedInputStream,釋放資源
                    pipedInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
        // 啟動執行緒1和執行緒2
        thread1.start();
        thread2.start();
    }
}

2.4 資料流

我們知道在Java中分為基本資料型別和引用型別,我們在做資料的讀取與寫入時,自然也會涉及到這種情況,比如我們將txt檔案中的數字型資料以int型別讀取到程式中,這時Java為我們提供了DataInputStream/DataOutputStream類。它們的常用方法為:

image

具體使用也相對比較簡單:

DataInputStream dis = new DataInputStream(new FileInputStream("input.txt"));
// 建立一個 DataOutputStream 物件,用於將資料寫入到檔案中
DataOutputStream das = new DataOutputStream(new FileOutputStream("output.txt"));
// 讀取四個位元組,將其轉換為 int 型別
int i = dis.readInt();
// 將一個 int 型別的資料寫入到檔案中
das.writeInt(1000);

2.5 緩衝流

對於資料的處理,CPU速度快於記憶體,記憶體又遠快於硬碟,在大資料量情況下,頻繁的透過IO向磁碟讀寫資料會帶來嚴重的效能問題,為此Java中提供了一個緩衝流的概念,簡單來說就是在記憶體中設定一個緩衝區,只有緩衝區中儲存的資料到達一定量後才會觸發一次IO,這樣大大提升了程式的讀寫效能,常用的緩衝流有:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。
image

透過BufferedInputStream的底層原始碼我們可以看到,其內部維護了一個buf[]資料,預設大小為8192位元組,我麼也可以透過建構函式進行快取大小設定。

public
class BufferedInputStream extends FilterInputStream {
    // 內部緩衝區陣列
    protected volatile byte buf[];
    // 緩衝區的預設大小
    private static int DEFAULT_BUFFER_SIZE = 8192;
    // 使用預設的緩衝區大小
    public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }
    // 自定義緩衝區大小
    public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
}

至於說緩衝流到底能不能實現效能的提升,我們實踐出真知,對於程式設計師來說所有的理論都不及上手寫一寫來得有效!這其實也涉及到一個經常被問的面試問題:java中的緩衝流真的效能很好嗎?

剛好,我們手頭有一本《Java效能權威指南》的PDF版,大小為66MB,我們透過普通的檔案流和緩衝流進行檔案的讀取和複製,看一下耗時對比。

public class TestService{
    public static void main(String[] args) throws IOException {
        TestService testService = new TestService();
        testService.copyPdfWithPublic();
        testService.copyPdfWithBuffer();
    }
    /*透過普通檔案流進行pdf檔案的讀取和複製*/
    public void copyPdfWithPublic(){
        // 記錄開始時間
        long start = System.currentTimeMillis();
        try (FileInputStream fis = new FileInputStream("E:\\Java效能權威指南.pdf");
             FileOutputStream fos = new FileOutputStream("E:\\Java效能權威指南Public.pdf")) {
            int content;
            while ((content = fis.read()) != -1) {
                fos.write(content);
            }
            //使用陣列充當快取時,兩者效能差距不大
            /*int len;
            byte[] bytes = new byte[4 * 1024];
            while ((len = fis.read(bytes)) != -1) {
                fos.write(bytes, 0, len);
            }*/
            fis.close();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 記錄結束時間
        long end = System.currentTimeMillis();
        System.out.println("使用普通檔案流複製PDF檔案總耗時:" + (end - start) + " 毫秒");
    }
    /*透過緩衝位元組流進行pdf檔案的讀取和複製*/
    public void copyPdfWithBuffer(){
        // 記錄開始時間
        long start = System.currentTimeMillis();
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("E:\\Java效能權威指南.pdf"));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("E:\\Java效能權威指南Buffer.pdf"))) {
            int content;
            while ((content = bis.read()) != -1) {
                bos.write(content);
            }
            /*int len;
            byte[] bytes = new byte[4 * 1024];
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }*/
            bis.close();
            bos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 記錄結束時間
        long end = System.currentTimeMillis();
        System.out.println("使用緩衝位元組流複製PDF檔案總耗時:" + (end - start) + " 毫秒");
    }
}

輸出:

使用普通檔案流複製PDF檔案總耗時:221611 毫秒
使用緩衝位元組流複製PDF檔案總耗時:228 毫秒

image

然後,我們將註釋掉的程式碼放開,也就是我們採用一個快取陣列,先將陣列儲存起來後,兩者之間的效能差距就沒那麼明顯了。

使用普通檔案流複製PDF檔案總耗時:106 毫秒
使用緩衝位元組流複製PDF檔案總耗時:80 毫秒

在這種情況下,我們可以看到,甚至於普通的檔案流的耗時是小於緩衝流的,所以對於這種情況來說,緩衝流未必一定效能最好。

2.6 列印流

對於System.out.println("Hello World");這句程式碼我想大家並不陌生吧,我們剛學習Java的第一堂課,老師們都會讓我們輸出一個Hello World,System.out 實際是用於獲取一個 PrintStream 物件,print方法實際呼叫的是 PrintStream 物件的 write 方法。

public class PrintStream extends FilterOutputStream
    implements Appendable, Closeable {
}
public class PrintWriter extends Writer {
}

結尾彩蛋

如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!
image

如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!
image

相關文章