一、BufferedInputStream介绍
/**
* A <code>BufferedInputStream</code> adds
* functionality to another input stream-namely,
* the ability to buffer the input and to
* support the <code>mark</code> and <code>reset</code>
* methods. When the <code>BufferedInputStream</code>
* is created, an internal buffer array is
* created. As bytes from the stream are read
* or skipped, the internal buffer is refilled
* as necessary from the contained input stream,
* many bytes at a time. The <code>mark</code>
* operation remembers a point in the input
* stream and the <code>reset</code> operation
* causes all the bytes read since the most
* recent <code>mark</code> operation to be
* reread before new bytes are taken from
* the contained input stream.
*
* @author Arthur van Hoff
* @since JDK1.0
*/
public class BufferedInputStream extends FilterInputStream
根据源码可知BufferedInputStream继承自FilterInputStream,通过在实例创建的时候在内部创建一个缓冲数组,基于装饰器设计模式为引用的其他输入流提供输入/读取缓冲并支持reset和mark方法。当内部引用的输入流读取字节(read)或者跳过字节数据(skip)时内部缓冲数组将被重新填充
二、BufferedInputStream源码分析
1)类继承结构
InputStream
|___FilterInputStream
|____BufferedInputStream
2)类成员变量
protected volatile InputStream in
FilterInputStream基于装饰器模式将InputStream封装到类内部的成员变量in,请注意它使用了volatile进行修饰,这意味着in的内存可见性为多线程可见
private static int DEFAULT_BUFFER_SIZE = 8192;
默认的缓冲数组大小
private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
最大数组大小限制,尝试分配更大数组可能会抛出内存溢出异常
protected volatile byte buf[];
内部缓冲数组的大小,他在缓冲区扩容时可能被另一个数组替换
private static final AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater = AtomicReferenceFieldUpdater.newUpdater(BufferedInputStream.class, byte[].class, "buf");
缓存数组的原子更新器,该成员变量与buf数组的volatile关键字共同组成了buf数组的原子更新功能的实现
protected int count;
该成员变量表示目前缓冲区存在的有效字节数,值在0和buf.length之间
protected int pos;
该成员变量表示当前缓冲区的读取位置,值在0和count之间,如果小于count那么buf[pos]表示输入流下一次读取的字节,如果等于count表示等待读取更多的输入流字节数据
protected int markpos = -1
标记位置,该标记位置主要用于实现流的标记特性,在流的某个位置标记之后继续读取允许通过调用reset方法将流的位置重置到该标记位置,但是输入流的标记长度不是无限的,当pos-markpos大于markLimit时,mark标记可能会被清除
protected int markLimit;
该成员变量表示上面提到的标记最大保留区间大小,即当pos-markpos>markLimit时,mark标记可能会被清除
3)BufferedInputStream成员方法
1 - 构造方法解析:
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
构造方法很简单,就是让类内部成员变量in引用外部传入的输入流对象in,并初始化缓冲区数组buf
2 - private void fill()方法
/**
* Fills the buffer with more data, taking into account
* shuffling and other tricks for dealing with marks.
* Assumes that it is being called by a synchronized method.
* This method also assumes that all data has already been read in,
* hence pos > count.
*/
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos < 0)
pos = 0; /* no mark: throw away the buffer */
else if (pos >= buffer.length) /* no room left in buffer */
if (markpos > 0) { /* can throw away early part of the buffer */
int sz = pos - markpos;
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) {
markpos = -1; /* buffer got too big, invalidate mark */
pos = 0; /* drop buffer contents */
} else if (buffer.length >= MAX_BUFFER_SIZE) {
throw new OutOfMemoryError("Required array size too large");
} else { /* grow buffer */
int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
pos * 2 : MAX_BUFFER_SIZE;
if (nsz > marklimit)
nsz = marklimit;
byte nbuf[] = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
// Can't replace buf if there was an async close.
// Note: This would need to be changed if fill()
// is ever made accessible to multiple threads.
// But for now, the only way CAS can fail is via close.
// assert buf == null;
throw new IOException("Stream closed");
}
buffer = nbuf;
}
count = pos;
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
}
fill()方法可以说是BufferedInputStream最核心也最重要的方法,该方法应该确保被一个线程同步的方法调用。它主要用于往缓冲区中填入更多的输入流数据,这其中还涉及到缓冲区数组替换,缓冲区有效字节数count的更新。它的主要处理逻辑如下:
(1)如果markpos小于0也就是没有标记,那么清空缓冲区buf的数据从输入流中载入下一段字节数据;
(2)如果markpos大于等于0且pos小于缓冲数组长度buf.length,那么意味着缓冲区存在未读取数据,结束方法不做处理;
(3)如果markpos大于等于0且pos大于等于缓冲数组长度buf.length,那么表示这时候缓冲区空间已经耗尽需要继续载入绑定输入流的下一段字节数据,这又要分4种情况处理:
1 - 当markpos大于0,表示已经标记过。为了节省空间且保证下一次调用reset方法可以从当前标记为重新读取,这里不对缓冲区直接扩容而是丢弃标记位markpos之前的缓冲数据,读取输入流下一段字节数据到缓冲数组buf剩余部分;
2 - 当buf.length>=markLimit且markpos==0也就是标记区间大于限制,那么直接丢弃所有缓冲数据,并重新载入输入流下一段字节数据到缓冲数组buf中;
3 - 当buf.length<markLimit且markpos==0说明缓冲区大小还没超过标记区间大小限制,还有扩容空间。那么按照规则扩容,扩容规则是:在不超出缓冲数组最大长度MAX_BUFFER_SIZE和标记区间大小限制markLimit的条件下扩容为原数组的2倍,超出取三者最小值,用扩容之后的数组填充原缓冲数组数据并替换它,继续读取绑定输入流数据填充到缓冲数组buf剩余部分。
fill方法的总结下来就是BufferedInputStream通过每次预先从输入流读取一段字节数据到缓冲区中,BufferedInputStream的读取操作实际是在缓冲区中进行,当读取的字节数据超出缓冲区范围那么再次从输入流中载入下一段字节数据到缓冲区中,这样做的好处是在大部分情况下外部输入流读取操作可以直接在缓冲区中获得数据减少大量的磁盘IO。
3 - int read()方法
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
该方法的作用与父类InputStream一样用于读取当前输入流的下一个字节数据,但是处理方式和其他类不一样,由于他对输入数据做了缓冲,因此优先判断pos是否大于count如果大于那么调用fill方法读取成员变量输入流in中字节数据并填充到缓冲数组buf中,如果到达输入流末尾即pos>count那么返回-1表示结束,否则返回缓冲区数组中pos指向的待读取输入流的下一字节数据。
4 - long skip(long n)
public synchronized long skip(long n) throws IOException {
getBufIfOpen(); // Check for closed stream
if (n <= 0) {
return 0;
}
long avail = count - pos;
if (avail <= 0) {
// If no mark position set then don't keep in buffer
if (markpos <0)
return getInIfOpen().skip(n);
// Fill in buffer to save bytes for reset
fill();
avail = count - pos;
if (avail <= 0)
return 0;
}
long skipped = (avail < n) ? avail : n;
pos += skipped;
return skipped;
}
skip方法用于在输入流读取中跳过指定字节数。他在方法开始会先调用getBufIfOpen检查输入流是否已经关闭,关闭抛出IO异常,接下来tgu通过count-pos计算缓冲数组剩余可读取的字节数avail,如果avail小于等于0也就是当前缓冲区已经耗尽需要从输入流中读取下一段字节数据到缓冲数组中,如果markpos小于0即输入流当前没有标记不需要缓存之后的字节数据,那么直接在输入流跳过指定数目字节,否则从输入流中载入下一段字节数据到缓冲数组中,如果载入后剩余可读字节数avail(count-pos)仍然小于等于0则说明已经到达输入流末尾无可读数据,结束返回0。跳转的字节数取缓冲数组剩余可读字节数和传入跳转字节数参数n的最小值,也就是说用户调用skip方法最终跳转的字节数不仅仅取决于传入参数n,而是受当前缓冲区读取进度状态,缓冲区长度、输入流是否有标记(markpos)以及标记区间大小markLimit以及传入参数n等多重因素影响,详细细节可以参考当前方法和fill方法源码。
5 - 其他成员方法
其他成员方法逻辑较为简单,可以自己基于核心方法fill()研究相应源码。