缓冲流使用
你有没有想过这个问题:
为什么加了
BufferedInputStream/BufferedOutputStream后 IO 操作就变快了?
答案是:系统调用太贵了。
IO 慢的本质
磁盘和网络 IO 比内存访问慢几个数量级。核心问题有两个:
- 系统调用开销:每次 read/write 都涉及用户态→内核态切换,这个上下文切换本身就很贵
- 数据拷贝次数:数据在用户内存和内核内存之间来回拷贝
系统调用开销 ≈ 数千个 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. 永远用缓冲流读写文件 │
└─────────────────────────────────────────────────────────────────┘