Java缓冲流(Buffered Stream)的内部工作原理是什么?
Java 中的缓冲流(Buffered Streams)是 I/O 操作中非常重要的性能优化工具。它们包括 BufferedInputStream、BufferedOutputStream、BufferedReader 和 BufferedWriter。
要理解它们的工作原理,我们需要从 “为什么需要缓冲流” 以及 “它的内部结构和机制” 两个方面来深入剖析。
一、 核心痛点:为什么需要缓冲流?
在没有缓冲流的情况下,直接使用 FileInputStream 或 FileOutputStream 进行读写:
- 每次读写都会触发一次操作系统调用(System Call)。
- 操作系统需要驱动磁盘(或网络)进行实际的物理 I/O 操作。
- 物理 I/O 非常慢,而 CPU 和内存的速度极快。如果每次只读写一个字节,CPU 将大量时间浪费在等待 I/O 设备响应上(上下文切换和硬件延迟)。
缓冲流的解决方案:
在内存中开辟一块缓冲区(Buffer)。
- 读操作:一次性从设备读取一大块数据(例如 8KB)放入内存缓冲区,后续的读取直接从内存缓冲区获取,直到缓冲区读空,再发起下一次物理读取。
- 写操作:数据先写入内存缓冲区,等缓冲区满了,或者手动触发刷新(flush),再将整个缓冲区的数据一次性写入设备。
二、 BufferedInputStream 的内部工作原理
我们以 BufferedInputStream 为例,看看它是如何工作的。
1. 核心成员变量(源码级别)
java
public class BufferedInputStream extends FilterInputStream {
// 内部缓冲区,默认大小为 8192 字节(8KB)
protected volatile byte buf[];
// 缓冲区中现有的有效字节数
protected int count;
// 当前读取到缓冲区的哪个位置(指针/索引)
protected int pos;
// 与 mark/reset 机制相关的变量
protected int markpos = -1;
protected int marklimit;
}
2. 读数据(read())的详细步骤
当你调用 bufferedInputStream.read() 读取一个字节时,内部会发生以下事情:
plaintext
[ 客户端请求 read() ]
│
┌────────┴────────┐
(pos < count?) 是否有缓存数据?
├─── 是 ───> [ 直接返回 buf[pos++] ]
│
否 (缓存用尽)
│
[ 调用 fill() 方法 ]
│
┌──────────┴──────────┐
从底层 InputStream 将新数据填入 buf
读取一大块数据(如8KB) 重置 pos=0, count=新读入字节数
└──────────┬──────────┘
│
[ 返回 buf[pos++] ]
- 检查缓冲区:判断
pos < count是否成立。如果成立,说明缓冲区里还有未读的数据。 - 内存直接返回:如果缓冲区有数据,直接返回
buf[pos++],这是一个纯内存操作,极其迅速(纳秒级)。 - 缓冲区耗尽(fill 过程):如果
pos >= count,说明缓冲区的数据已经读完了。此时会调用内部的fill()方法:fill()会调用底层的InputStream.read(buf, ...),向操作系统请求填充这个 8KB 的数组。- 这是一次真实的 I/O 操作(慢)。
- 数据填满
buf后,重置pos = 0,并将count设为实际读入的字节数。
- 返回数据:再次从
buf[pos++]读取字节返回给用户。
三、 BufferedOutputStream 的内部工作原理
BufferedOutputStream 的逻辑正好相反,它是为了减少写入磁盘/网络的次数。
1. 核心成员变量
java
public class BufferedOutputStream extends FilterOutputStream {
// 内部缓冲区,默认 8KB
protected byte buf[];
// 缓冲区中当前已写入的字节数
protected int count;
}
2. 写数据(write())的详细步骤
当你调用 bufferedOutputStream.write(int b) 写入一个字节时:
- 检查缓冲区是否已满:判断
count >= buf.length。 - 缓冲区未满:直接将数据放入内存数组:
buf[count++] = (byte)b。这是一个内存操作,瞬间完成。 - 缓冲区已满(flush 过程):如果缓冲区满了,调用内部的
flushBuffer()方法:- 调用底层
OutputStream.write(buf, 0, count),将这 8KB 的数据一次性写入目的地(真实的 I/O)。 - 将
count重置为 0。 - 将当前要写的这个新字节放入
buf[0],count变为 1。
- 调用底层
3. 为什么必须调用 close() 或 flush()?
如果最后一次写入时,缓冲区只填了 2KB(没满),程序就结束了。因为没有触发“缓冲区满”的条件,这 2KB 数据将一直留在内存中,不会写入磁盘。
flush()会强制将缓冲区中剩余的 2KB 数据写入底层流。close()内部会先调用flush(),然后关闭流。因此,使用完输出流必须关闭。
四、 字符缓冲流:BufferedReader / BufferedWriter
它们的原理与字节缓冲流完全一致,主要区别在于:
- 数据单位不同:
BufferedInputStream内部是byte[] buf(字节数组,默认 8192 字节)。BufferedReader内部是char[] cb(字符数组,默认 8192 个字符)。
- 功能增强:
BufferedReader提供了readLine()方法(按行读取)。它通过在缓冲区中寻找\n或\r来拆分字符串。BufferedWriter提供了newLine()方法,自动根据操作系统写入换行符。
五、 设计模式:装饰器模式(Decorator Pattern)
缓冲流并没有自己实现读写文件的功能,它们必须包裹一个底层的流(如 FileInputStream)。
java
InputStream in = new BufferedInputStream(new FileInputStream("test.txt"));
这是典型的装饰器模式:
FileInputStream提供了基础的“读”功能(但很慢)。BufferedInputStream是“装饰器”,它继承了InputStream,并持有FileInputStream的引用。- 它在不改变原有接口的前提下,为“读”功能添加了缓冲区包装,从而极大提升了性能。
总结
缓冲流的本质就是“空间换时间”。
| 步骤 | 无缓冲流 | 有缓冲流 |
|---|---|---|
| 读/写 8000 次 1 字节 | 触发 8000 次系统调用(极慢) | 触发 1 次系统调用,其余 7999 次为内存操作(极快) |
| 数据中介 | JVM 内存 ⇄ 操作系统 ⇄ 磁盘 | JVM 内存 ⇄ 缓冲流数组(8KB) ⇄ 操作系统 ⇄ 磁盘 |
右滑查看面试常问