参考文章:
《java.io.BufferedInputStream 源码分析》
《IO源码解析--一文说尽BufferedInputStream》
《JavaIO之BufferedInputStream详解》
写在开头:本文为学习后的总结,可能有不到位的地方,错误的地方,欢迎各位指正。
前段时间遇到个需求涉及到了IO,趁这机会重新复习了下,这里简单记录下对缓冲流的理解。
Java中的缓冲流BufferedInputStream继承自字节流,通过减少从磁盘读取文件的次数实现了文件读写的提速。可以简单的理解为,直接使用FileInputStream每次从磁盘读取1KB数据进入内存,而缓冲流直接每次读取8KB,通过这样的方式减少了磁盘的读取次数。
Object(java.lang)
-- InputStream(java.io)
-- FilterInputStream(java.io)
-- BufferedInputStream(java.io)
目录
2、public synchronized int read()
3、private int read1(byte[] b, int off, int len)
4、public synchronized int read(byte b[], int off, int len)
一、基本功能介绍
// 默认的缓冲池大小 8KB
private static int DEFAULT_BUFFER_SIZE = 8192;
// 最大的缓冲池大小(-8的原因是兼容某些虚拟机数组自带的头信息)
private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
// 缓冲数组
protected volatile byte buf[];
// 原子更新器,保证了数组的原子性,防止在 buffer 被关闭的情况下修改 buffer 数组.判断 buffer 是否被关闭的条件是 buf 数组是否为 null
private static final
AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
AtomicReferenceFieldUpdater.newUpdater
(BufferedInputStream.class, byte[].class, "buf");
// 当前缓冲区末尾的位置
protected int count;
// 当前缓冲区中读取位置的索引
protected int pos;
// 是否开启重复读的标记位
// markpos和reset()配合使用才有意义.操作步骤:
// 1.通过mark() 函数,保存pos的值到markpos中。
// 2.通过reset() 函数,会将pos的值重置为markpos。
protected int markpos = -1;
// 可重复读的最大长度,即markpos到pos的长度限制
protected int marklimit;
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];
}
count、pos、markpos的位置关系如下。
BufferedInputStream在内部构建一个缓冲池,每次会从数据源获取一定容量的数据填充到缓冲池中,在这个缓冲池中,pos是当前读取位置的索引,count是缓冲池末尾的位置。markpos默认值-1,可以通过mark(marklimit)方法将值设置为大于等于0,此时我们可以调用reset方法对已读取过的文件进行重复读取(即从markpos到pos这段的数据)。但这个重复读的长度限制也是有限制的,即marklimit。
二、主体方法
1、流的操作
1.1、getInIfOpen():获取底层 InputStream 的对象,如果流被关闭就抛异常.该方法除了获取底层流还可以判断流是否关闭,通过抛异常终端后续操作.
1.2、getBufIfOpen():获取 buffer 数组对象引用,如果流被关闭就抛异常.所以该方法除了获取底层流还可以判断流是否关闭,通过抛异常终端后续操作.
1.3、close(): 操作很简单,就是置空缓存数组和关闭流.
// 获取输入流
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;
}
// 关闭输入流
public void close() throws IOException {
byte[] buffer;
while ( (buffer = buf) != null) {
if (bufUpdater.compareAndSet(this, buffer, null)) {
InputStream input = in;
in = null;
if (input != null)
input.close();
return;
}
// Else retry in case a new buf was CASed in fill()
}
}
2、辅助功能
1. available():返回还有多少字节可读。n代表当前缓冲区的可读字节数,avail代表数据源的可读字节数。返回 (n + avail) 与 MAX_VALUE 中较小的一方。
// 返回还有多少字节可读
public synchronized int available() throws IOException {
int n = count - pos;
int avail = getInIfOpen().available();
return n > (Integer.MAX_VALUE - avail)
? Integer.MAX_VALUE
: n + avail;
}
2. skip():跳过一定数量字节
计算当前缓冲区的未写出字节数为avail:
(1)如果avail大于0,跳过n与avail中较小的数量的字节,返回这个较小的数。
(2)如果avail小于等于0:
(2.1)无标记,直接在源输入流跳过n个字节。
(2.2)有标记,将被标记的那段缓存往左移动首处,然后重新计算avail,如果仍然小于等于0,则返回0,代表跳过了0个字节
// 跳过一定数量字节
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;
}
3. mark():开启重复读功能,参数readlimit用来限制重复读的最大长度,同时将当前读取位置的索引pos赋值给markpos,供reset使用
// 开启重复读功能
public synchronized void mark(int readlimit) {
marklimit = readlimit;
markpos = pos;
}
4. reset():重置读取位置,将读取位置pos重新定位到之前调用mark方法时标记的重复读的起始位置。
// 重置函数
public synchronized void reset() throws IOException {
getBufIfOpen(); // Cause exception if closed
if (markpos < 0)
throw new IOException("Resetting to invalid mark");
pos = markpos;
}
5. fill():填充缓冲区。fill方法是缓冲区填充的重要逻辑,这里我再拆分成几块来简化理解。
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;
}
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos < 0){
// (1) 未启用重复读 markpos = -1
}else if (pos >= buffer.length){
// (2) 启用重复读 markpos >=0
if (markpos > 0){ // (2.1)markpos > 0
}else if (buffer.length >= marklimit){ // (2.2)markpos == 0
}else if (buffer.length >= MAX_BUFFER_SIZE) { // (2.3)溢出判断
}else{ // (2.4)正常扩容
}
}
count = pos;
// 从数据源获取数据
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
// 读取数据后计算出新的 count
count = n + pos;
}
(1)markpos < 0:未开启重复读功能,直接将pos设置为0,从头读取
(2)markpos >=0且pos>=buffer.length:
(2.1)markpos > 0 :
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;
}
因为markpos>0,所以必然有pos - markpos<buffer.length,需要把markpos-pos这一段
复制到buffer的开头进行保存,同时将markpos置位0。
(2.2)buffer.length >= marklimit:
else if (buffer.length >= marklimit) {
markpos = -1; /* buffer got too big, invalidate mark */
pos = 0; /* drop buffer contents */
}
这一步根据上面的判断筛选后必然存在markpos==0,相当于需要重复读的长度为整个buffer数组,这里就需要确认buffer.length是否超过了允许重复读的长度上限marklimit。因此直接将markpos置为-1,关闭重复读,同时将pos置为0,buffer从头开始写。
(2.3)buffer.length >= MAX_BUFFER_SIZE:数组长度大于规定容量最大值 MAX_BUFFER_SIZE,内存溢出。
else if (buffer.length >= MAX_BUFFER_SIZE) {
throw new OutOfMemoryError("Required array size too large");
}
(2.4)正常扩容:
else {
// pos <= MAX_BUFFER_SIZE - pos 相当于判断 pos 大小是否大于 MAX_BUFFER_SIZE 的一半
// nsz 为 buffer 容量扩充后的大小
// pos >= MAX_BUFFER_SIZE/2 时 nsz = MAX_BUFFER_SIZE
// pos < MAX_BUFFER_SIZE/2 时 nsz = pos * 2
int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
pos * 2 : MAX_BUFFER_SIZE;
if (nsz > marklimit)
// nsz 大小不可以超过 marklimit
nsz = marklimit;
// 创建大小为 nsz 新的 buffer 数组
byte nbuf[] = new byte[nsz];
// 把旧数组中的数据复制到新的数组中
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
// 在高并发场景下,调用 compareAndSet() 可保证流还没有被关闭
throw new IOException("Stream closed");
}
// 把 buffer 底层字节数组换成新的扩容后的数组
buffer = nbuf;
}
取2倍pos,marklimit以及MAX_BUFFER_SIZE三者中最小的数为size,创建一个byte数组,并将当前缓冲区的所有数据拷贝到这个扩大后的数组中,另这个扩大后的数组为当前类的缓冲区。
三、读取方法
1、public int read(byte b[])
// FilterInputStream.java
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
2、public synchronized int read()
/**
* 返回下一个字节
*/
public synchronized int read() throws IOException {
if (pos >= count) {
// pos >= count 表示当前 buffer 内的所有数据都读完了
// 调用 fill() 往 buffer 内添加新的数据
fill();
// 如果 pos >= count 依然成立,返回 -1 表示数据已读完
if (pos >= count)
return -1;
}
// 返回 buffer 有效数据的第一个字节
return getBufIfOpen()[pos++] & 0xff;
}
注意:最后返回之前还要做一次位运算,原因是 buf[] 是 byte 数组,read() 返回的是 int ,如何把一个 byte 变成一个 int 呢,答案就是补零,把 8 位的 byte 变成 32位的 int.
3、private int read1(byte[] b, int off, int len)
/**
* @param b 读取数据存放的目标数组
* @param off 偏移值,表示 b 数组可以写的第一个字节的下标
* @param len 读取的数据大小
*/
private int read1(byte[] b, int off, int len) throws IOException {
// avail 表示缓存数组 buf[] 可写空间大小
int avail = count - pos;
// avail <= 0 表示缓存数组已写满
if (avail <= 0) {
/* If the requested length is at least as large as the buffer, and
if there is no mark/reset activity, do not bother to copy the
bytes into the local buffer. In this way buffered streams will
cascade harmlessly. */
/* 如果读取的大小 >= 缓存 buffer 数组长度,且 mark 机制是无效的时候,
不用纠结还想着先把数据复制到缓存中,直接从 InputStream 中读取数据即可.*/
if (len >= getBufIfOpen().length && markpos < 0) {
// 内部调用了 read()
return getInIfOpen().read(b, off, len);
}
// 往缓存中添加数据
fill();
// 如果当前
avail = count - pos;
if (avail <= 0) return -1;
}
// cnt 表示需要复制的数据长度
int cnt = (avail < len) ? avail : len;
// 从 buffer 中获取数据范围 [pos, pos+cnt] 数据到 b 中范围 [off, off+cnt] 中
System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
// 更新缓存中下一个可读字节下标 pos
pos += cnt;
return cnt;
}
4、public synchronized int read(byte b[], int off, int len)
// BufferedInputStream.java
/**
* @param b 读取数据存放的目标数组
* @param off 偏移值,表示 b 数组可以写的第一个字节的下标
* @param len 读取的数据大小
*/
public synchronized int read(byte b[], int off, int len)
throws IOException
{
// 检查流是否被关闭
getBufIfOpen();
// 对 read() 传入参数做校验,防止数组边界越界
if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
// n 记录已读数据大小
int n = 0;
for (;;) {
// 调用 read1 方法往数组 b 中添加数据, nread 为添加的数据大小
int nread = read1(b, off + n, len - n);
if (nread <= 0)
return (n == 0) ? nread : n;
n += nread;
// 满足 nread > 0 && n >= len
// 判断数据是否已经读完了
if (n >= len)
return n;
InputStream input = in;
// 满足 nread > 0 && n < len
// 判断 inputstream 中是否还有未读数据
if (input != null && input.available() <= 0)
return n;
}
}