基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Java缓冲流(Buffered Stream)的内部工作原理是什么?

Java 中的缓冲流(Buffered Streams)是 I/O 操作中非常重要的性能优化工具。它们包括 BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter

要理解它们的工作原理,我们需要从 “为什么需要缓冲流” 以及 “它的内部结构和机制” 两个方面来深入剖析。


一、 核心痛点:为什么需要缓冲流?

在没有缓冲流的情况下,直接使用 FileInputStreamFileOutputStream 进行读写:

  • 每次读写都会触发一次操作系统调用(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++] ]
  1. 检查缓冲区:判断 pos < count 是否成立。如果成立,说明缓冲区里还有未读的数据。
  2. 内存直接返回:如果缓冲区有数据,直接返回 buf[pos++],这是一个纯内存操作,极其迅速(纳秒级)。
  3. 缓冲区耗尽(fill 过程):如果 pos >= count,说明缓冲区的数据已经读完了。此时会调用内部的 fill() 方法:
    • fill() 会调用底层的 InputStream.read(buf, ...),向操作系统请求填充这个 8KB 的数组。
    • 这是一次真实的 I/O 操作(慢)。
    • 数据填满 buf 后,重置 pos = 0,并将 count 设为实际读入的字节数。
  4. 返回数据:再次从 buf[pos++] 读取字节返回给用户。

三、 BufferedOutputStream 的内部工作原理

BufferedOutputStream 的逻辑正好相反,它是为了减少写入磁盘/网络的次数。

1. 核心成员变量

java
public class BufferedOutputStream extends FilterOutputStream {
    // 内部缓冲区,默认 8KB
    protected byte buf[];
    
    // 缓冲区中当前已写入的字节数
    protected int count;
}

2. 写数据(write())的详细步骤

当你调用 bufferedOutputStream.write(int b) 写入一个字节时:

  1. 检查缓冲区是否已满:判断 count >= buf.length
  2. 缓冲区未满:直接将数据放入内存数组:buf[count++] = (byte)b。这是一个内存操作,瞬间完成。
  3. 缓冲区已满(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

它们的原理与字节缓冲流完全一致,主要区别在于:

  1. 数据单位不同
    • BufferedInputStream 内部是 byte[] buf(字节数组,默认 8192 字节)。
    • BufferedReader 内部是 char[] cb(字符数组,默认 8192 个字符)。
  2. 功能增强
    • 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) ⇄ 操作系统 ⇄ 磁盘
00:00
00:00