背景
BufferedInputStream和其他InputStream常常放在一起使用:
BufferedInputStream是套在某个其他的InputStream外,起着缓存的功能,用来改善里面那个InputStream的性能,它自己不能脱离里面那个单独存在。
比如FileInputStream是读取一个文件来作InputStream。所以你可以把BufferedInputStream套在FileInputStream外,来改善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;
}
....
}
主要来看非native的read()
方法。如果用read()
方法读取一个文件,每读取一个字节就要访问一次硬盘,这种读取的方式效率是很低的。即便使用read(byte b[])
方法一次读取多个字节,当读取的文件较大时,也会频繁的对磁盘操作。
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){
//为减少文章篇幅,源码就不显示了
}
}
在创建 BufferedInputStream时,会创建一个内部缓冲区数组。在读取流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。
也就是说,Buffered类初始化时会创建一个较大的byte数组,一次性从底层输入流中读取多个字节来填充byte数组,当程序读取一个或多个字节时,可直接从byte数组中获取,当内存中的byte读取完后,会再次用底层输入流填充缓冲区数组。
这种从直接内存中读取数据的方式要比每次都访问磁盘的效率高很多。
装饰器模式
注意我们看到BufferedInputStream的构造方法:
protected volatile InputStream in;
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
注意super(in)
,BufferedInputStream继承自FileInputStream。实际上做读写的时候,都是调用的传入构造函数的InputSteam。
BufferedInputStream为装饰者类,FileInputStream为被装饰者类,前者的作用就是为了加强后者已有的功能,这里就是为了提高数据流的读写效率。
BufferedInputStream的构造方法定义:public BufferedInputStream(InputStream in)可以看出,Buffered可以装饰任何一个InputSteam的 子类。
总结
FileInputStream是字节流,BufferedInputStream是字节缓冲流,使用BufferedInputStream读资源比FileInputStream读取资源的效率高(BufferedInputStream的read方法会读取尽可能多的字节,执行read时先从缓冲区读取,当缓冲区数据读完时再把缓冲区填满。)
因此,当每次读取的数据量很小时,FileInputStream每次都是从硬盘读入,而BufferedInputStream大部分是从缓冲区读入。读取内存速度比读取硬盘速度快得多,因此BufferedInputStream效率高,且FileInputStream对象的read方法会出现阻塞;BufferedInputStream的默认缓冲区大小是8192字节。当每次读取数据量接近或远超这个值时,两者效率就没有明显差别了。