Skip to content

缓冲流使用

你有没有想过这个问题:

为什么加了 BufferedInputStream / BufferedOutputStream 后 IO 操作就变快了?

答案是:系统调用太贵了

IO 慢的本质

磁盘和网络 IO 比内存访问慢几个数量级。核心问题有两个:

  1. 系统调用开销:每次 read/write 都涉及用户态→内核态切换,这个上下文切换本身就很贵
  2. 数据拷贝次数:数据在用户内存和内核内存之间来回拷贝
系统调用开销 ≈ 数千个 CPU 周期
一次内存操作 ≈ 几十个 CPU 周期

差距是100倍

有缓冲 vs 无缓冲:测试数据说话

有人做过测试,读取同一个 1GB 文件:

方式系统调用次数耗时
每字节调用一次 read()~10亿次几十秒
用 8KB 缓冲区批量读~12万次几秒
用 64KB 缓冲区批量读~1.5万次1秒内

结论:缓冲区越大,系统调用越少,性能越好(但也不能无限大)。

缓冲流的工作原理

缓冲流在内存中维护一个缓冲区,数据先读写到缓冲区,满时才触发真实的 IO 操作:

应用代码 ──read()──▶  BufferedInputStream(缓冲区 8KB)

                         │ 缓冲区有数据?
                         ├── 是 → 直接返回(内存操作,极快)
                         └── 否 → 从磁盘一次读 8KB 到缓冲区

不加缓冲 vs 加缓冲

java
// ❌ 灾难级:每读一个字节触发一次系统调用
try (FileInputStream fis = new FileInputStream("large.dat")) {
    while (fis.read() != -1) {
        // 处理数据
    }
}
// 1GB 文件 = 10亿次系统调用 = 可能几十秒

// ✅ 正常:批量读
try (BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("large.dat"))) {
    byte[] buf = new byte[8192];
    while (bis.read(buf) != -1) {
        // 处理数据
    }
}
// 1GB 文件 = 约 12万次系统调用 = 显著快

// ✅ 推荐:加 Buffered 包装 + 指定缓冲区大小
try (BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("large.dat"), 64 * 1024)) { // 64KB 缓冲区
    byte[] buf = new byte[8192];
    while (bis.read(buf) != -1) {
        // 处理数据
    }
}

典型的生产级写法

装饰器模式允许无限嵌套:

java
// 典型的生产级写法
try (
    BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("source.dat"), 64 * 1024);
    BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("target.dat"), 64 * 1024)
) {
    byte[] buffer = new byte[8192];
    int len;
    while ((len = bis.read(buffer)) != -1) {
        bos.write(buffer, 0, len);
    }
}
  • BufferedInputStream(64KB):从磁盘一次读 64KB 到缓冲区
  • read(buffer):从缓冲区取数据(内存操作,极快)
  • BufferedOutputStream(64KB):先写到缓冲区,满了自动 flush 到磁盘

读写流程图

BufferedOutputStream 写入流程

应用 write("hello")


BufferedOutputStream 缓冲区(默认 8KB)

       ▼ 缓冲区满了?
   ├── 是 → flush() → 一次性 write(8KB) 到磁盘
   └── 否 → 等待,累积数据

BufferedInputStream 读取流程

应用 read()


BufferedInputStream 缓冲区(默认 8KB)

       ▼ 缓冲区有数据?
   ├── 是 → 直接返回(内存操作,极快)
   └── 否 → fill() → 一次性 read(8KB) 从磁盘

flush() 的时机

BufferedOutputStream 在缓冲区满时自动 flush,但有些场景需要手动 flush:

java
// 实时场景:日志写到文件,每条都要立刻落盘
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("app.log"))) {
    for (String log : logs) {
        bos.write(log.getBytes());
        bos.flush(); // 每条日志都要落盘
    }
}

// 非实时场景:批量写,最后一起 flush
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("data.obj"))) {
    for (Object obj : objects) {
        oos.writeObject(obj); // 写到缓冲区
    }
} // try-with-resources 结束时自动 flush

字符流缓冲

字符流的缓冲同样重要:

java
// ❌ 无缓冲的 FileReader(每次读一个 char,可能触发多次系统调用)
try (FileReader fr = new FileReader("file.txt")) {
    while (fr.read() != -1) { }
}

// ✅ 有缓冲的 BufferedReader(批量读,readLine 高效)
try (BufferedReader br = new BufferedReader(
        new FileReader("file.txt"))) {
    String line;
    while ((line = br.readLine()) != null) { // 一次读一行
        System.out.println(line);
    }
}

// ✅ 更好的做法:指定编码 + 缓冲
try (BufferedReader br = new BufferedReader(
        new InputStreamReader(
            new FileInputStream("file.txt"), StandardCharsets.UTF_8),
        16 * 1024)) { // 16KB 缓冲区
    String line;
    while ((line = br.readLine()) != null) {
        // 处理
    }
}

常见误区

误区一:加了 Buffered 就够了,不用管缓冲区大小

java
// 默认缓冲区只有 8KB,对于大文件可以加大
// 8KB → 64KB:减少 8 倍的系统调用次数
new BufferedInputStream(fis, 64 * 1024)
new BufferedOutputStream(fos, 64 * 1024)

误区二:所有场景都用同一个缓冲区大小

java
// 网络 IO:MTU = 1460 字节,用 8KB 或 16KB 足够
// 大文件复制:用 64KB ~ 1MB
// 小文件频繁读写:用默认 8KB 即可

误区三:BufferedOutputStream 不需要手动 flush

java
// 不 flush,数据可能还在缓冲区,没写到磁盘
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("file.txt"))) {
    bos.write("hello".getBytes());
    // 没 flush,没 close
    // 程序退出时数据可能丢失
}

总结

┌─────────────────────────────────────────────────────────────────┐
│  缓冲流的核心作用:减少系统调用次数                               │
│                                                                 │
│  记住三点:                                                      │
│  1. 缓冲区大小根据场景调整(一般 8KB~64KB)                       │
│  2. flush() 控制何时真正写入磁盘                                  │
│  3. 永远用缓冲流读写文件                                         │
└─────────────────────────────────────────────────────────────────┘

基于 VitePress 构建