深入理解BufferedInputStream实现原理
原文写的好,自己转载加点补充。
通过分析FileInputStream类和BufferedInputStream类中的部分核心代码来理解带缓冲的字节输入流的实现原理,缓冲输出流原理与之相同,在此不再赘述。
FileInputStream源码
package java.io; public class FileInputStream extends InputStream{ /** *从输入流中读取一个字节 *该方法为private私有方法,用户不能直接调用。 *该方法为native本地方法,这是因为Java语言不能直接与操作系统或计算机硬件交互, *只能通过调用C/C++编写的本地方法来实现对磁盘数据的访问。 */ private native int read0() throws IOException; //调用native方法read0()每次读取一个字节 public int read() throws IOException { Object traceContext = IoTrace.fileReadBegin(path); int b = 0; try { b = read0(); } finally { IoTrace.fileReadEnd(traceContext, b == -1 ? 0 : 1); } return b; } /** * 从输入流中读取多个字节到byte数组中 * 该方法也是私有本地方法,不对用户开放,只供内部调用。 */ private native int readBytes(byte b[], int off, int len) throws IOException; //调用native方法readBytes(b, 0, b.length)每次读取多个字节 public int read(byte b[]) throws IOException { Object traceContext = IoTrace.fileReadBegin(path); int bytesRead = 0; try { bytesRead = readBytes(b, 0, b.length); } finally { IoTrace.fileReadEnd(traceContext, bytesRead == -1 ? 0 : bytesRead); } return bytesRead; } //从此输入流中将最多 len 个字节的数据读入一个 byte 数组中。 public int read(byte b[], int off, int len) throws IOException { Object traceContext = IoTrace.fileReadBegin(path); int bytesRead = 0; try { bytesRead = readBytes(b, off, len); } finally { IoTrace.fileReadEnd(traceContext, bytesRead == -1 ? 0 : bytesRead); } return bytesRead; } }
通过源码可以看到,如果用read()方法读取一个文件,每读取一个字节就要访问一次硬盘,这种读取的方式效率是很低的。即便使用read(byte b[])方法一次读取多个字节,当读取的文件较大时,也会频繁的对磁盘操作。
为了提高字节输入流的工作效率,Java提供了BufferedInputStream类。
首先解释一下BufferedInputStream的基本原理:
API文档的解释:在创建 BufferedInputStream时,会创建一个内部缓冲区数组。在读取流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。
也就是说,Buffered类初始化时会创建一个较大的byte数组,一次性从底层输入流中读取多个字节来填充byte数组,当程序读取一个或多个字节时,可直接从byte数组中获取,当内存中的byte读取完后,会再次用底层输入流填充缓冲区数组。
这种从直接内存中读取数据的方式要比每次都访问磁盘的效率高很多。下面通过分析源码,进一步理解其原理:
BufferedInputStream源码
package java.io; public class BufferedInputStream extends FilterInputStream { //缓冲区数组默认大小8192Byte,也就是8K private static int defaultBufferSize = 8192; /** * 内部缓冲数组,会根据需要进行填充。 * 大小默认为8192字节,也可以用构造函数自定义大小 */ protected volatile byte buf[]; /** * 缓冲区中还没有读取的字节数 * 当count=0时,说明缓冲区内容已读完,会再次填充 */ protected int count; // 缓冲区指针,记录缓冲区当前读取位置 protected int pos; //真正读取字节的还是InputStream private InputStream getInIfOpen() throws IOException { InputStream input = in; if (input == null) throw new IOException("Stream closed"); return input; } //创建空缓冲区 private byte[] getBufIfOpen() throws IOException { byte[] buffer = buf; if (buffer == null) throw new IOException("Stream closed"); return buffer; } //创建默认大小的BufferedInputStream public BufferedInputStream(InputStream in) { this(in, defaultBufferSize); } //此构造方法可以自定义缓冲区大小 public BufferedInputStream(InputStream in, int size) { super(in); if (size <= 0) { throw new IllegalArgumentException("Buffer size <= 0"); } buf = new byte[size]; } /** * 填充缓冲区数组 * 具体实现算法,可以看一下毕向东老师的视频,讲解的很详细, */ private void fill() throws IOException { byte[] buffer = getBufIfOpen(); if (markpos < 0) pos = 0; //....部分源码省略 count = pos; int n = getInIfOpen().read(buffer, pos, buffer.length - pos); if (n > 0) count = n + pos; } /** * 读取一个字节 * 与FileInputStream中的read()方法不同的是,这里是从缓冲区数组中读取了一个字节 * 也就是直接从内存中获取的,效率远高于前者 */ public synchronized int read() throws IOException { if (pos >= count) { fill(); if (pos >= count) return -1; } return getBufIfOpen()[pos++] & 0xff; } //从缓冲区中一次读取多个字节 private int read1(byte[] b, int off, int len) throws IOException { int avail = count - pos; if (avail <= 0) { if (len >= getBufIfOpen().length && markpos < 0) { return getInIfOpen().read(b, off, len); } fill(); avail = count - pos; if (avail <= 0) return -1; } int cnt = (avail < len) ? avail : len; System.arraycopy(getBufIfOpen(), pos, b, off, cnt); pos += cnt; return cnt; } public synchronized int read(byte b[], int off, int len){ //为减少文章篇幅,源码就不显示了 } }
最后我们通过一个实例更直观的说明BufferedStream的高效率。
import java.io.*; /** * 分别用普通数据流和带缓冲区的数据流复制一个167M的数据文件 * 通过用时比较两者的工作效率 * @author Zues * */ public class CopyMp3 { private static File file = new File("D:\\1.mp4"); private static File file_cp = new File("D:\\1_cp.mp4"); // FileInputStream复制 public void copy() throws IOException { FileInputStream in = new FileInputStream(file); FileOutputStream out = new FileOutputStream(file_cp); byte[] buf = new byte[1024]; int len = 0; while ((len = in.read(buf)) != -1) { out.write(buf); } in.close(); out.close(); } // BufferedStream复制 public void copyByBuffer() throws IOException { BufferedInputStream in = new BufferedInputStream(new FileInputStream(file)); BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file_cp)); byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) != -1) { out.write(buf); } in.close(); out.close(); } public static void main(String[] args) throws IOException { CopyMp3 copy=new CopyMp3(); long time1=System.currentTimeMillis(); copy.copy(); long time2=System.currentTimeMillis(); System.out.println("直接复制用时:"+(time2-time1)+"毫秒"); long time3=System.currentTimeMillis(); copy.copyByBuffer(); long time4=System.currentTimeMillis(); System.out.println("缓冲区复制用时:"+(time4-time3)+"毫秒"); } }
当复制一段379M的视频文件时:
直接复制用时:3155毫秒
缓冲区复制用时:865毫秒
通过实验可以看出带缓冲区的数据流效率远远高于普通的数据流,而且操作的文件越大,优势越明显。
补充原作者:(个人心得体会)
从BufferedInputStream的read()源码看出:
可以从硬盘一次读取8192字节,第一次read()或者已经读完8192字节了一定会执行到里面的fill()方法,会填满buffer数组或者将数据读完(数据少的情况下)。第二次读的时候直接从buffer数组(在内存)取出来,其中有一个pos下标,该下标的元素和0xff相&,即可获得一个字节的元素,此时就不是从硬盘操作,而是在内存中直接操作,速度快很多。BufferedInputStream的read()在硬盘读取一次,在内存操作多次,比FileInputStream的read()每次都从硬盘读取要快得多。
BufferedInputStream设计模式说明:
这里用到了装设者设计模式,BufferedInputStream为装饰者类,FileInputStream为被装饰者类,前者的作用就是为了加强后者已有的功能,这里就是为了提高数据流的读写效率。
BufferedInputStream的构造方法定义:public BufferedInputStream(InputStream in)可以看出,Buffered可以装饰任何一个InputSteam的 子类。
【如有理解错误的地方,还望大神们指正,谢谢。】
关于read方法读取内容
1.没有markpos的情况很简单:
2有mark的情况比较复杂
========================================Talk is cheap, show me the code=======================================
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?