1.概述

BufferedInputStream、BufferedOutputStream是带有缓冲区的字节处理流,默认的缓冲区字节数组大小为8192(8KB),通过装饰器模式对InputStream、OutputStream的功能进行增强,每次读写都是基于内部缓冲区操作,当缓冲区存储的字节超过一定数量时,才会进行真正的磁盘IO,这样能够有效减少磁盘的访问次数,从而提高读写的性能。

虽然缓冲处理流能装饰一切InputStream、OutputStream的子类,但是对序列化流、数据处理流等的装饰毫无意义,因为读写规则比较特殊,对数组字节流的装饰更是画蛇添足,因为这种流本身就基于内存。因此缓冲处理流的装饰对象基本都是文件、网络等,这种场景才能减少与操作系统或IO设备的交互次数。

2.简单使用

2.1 数据写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws Exception{

// 创建文件输入流
FileInputStream fis = new FileInputStream("you path");

// 创建缓冲输入流,进行装饰
BufferedInputStream bufis = new BufferedInputStream(fis);

// 创建缓冲
byte[] bytes = new byte[1024];

// 开始读取
int len = 0;
while((len = bufis.read(bytes))!=-1) {
System.out.println(new String(bytes, 0, len));
}
bufis.close();
}

2.2 数据读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception{

// 创建文件输入流
FileOutputStream fos = new FileOutputStream("you path");

// 创建缓冲输入流,进行装饰
BufferedOutputStream bufos = new BufferedOutputStream(fos);

// 创建缓冲
bufos.write(1);

// 落盘
bufos.flush();

// 关闭流
bufis.close();
}

3.BufferedInputStream

3.1 成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 缓冲区字节数组的默认大小,可以通过构造器初始化,覆盖此值
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");

// 缓冲区中可用的字节数量(包括已读和未读的)
protected int count;

// 下次read读取的缓冲区坐标
protected int pos;

// 支持重复读取情况下,重置后从该坐标重复读取,mark坐标以及往后的字节需要保留,用于reset后仍然能读到
// 但是缓冲区不可能为了这段数据无限扩容,字节总数超过marklimit,mark标记会变得无效
protected int markpos = -1;

// mark后缓冲区能保存的字节,在数量上的极限
protected int marklimit;

3.2 mark & reset

BufferedInputStream支持流的重复读取,而这个功能就是通过mark和reset方法进行实现。以单字节方式读取一段缓冲流为例:
图片

方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 标记方法
public synchronized void mark(int readlimit) {
marklimit = readlimit;
markpos = pos;
}

// 重置方法
public synchronized void reset() throws IOException {
getBufIfOpen();
if (markpos < 0)
throw new IOException("Resetting to invalid mark");
pos = markpos;
}

mark与reset方法是InputStream提供的接口,在这里讲解主要是因为使用标记后,会让BufferedInputStream的缓冲区管理变得复杂,因此只有搞清楚这块的逻辑,才能研究接下来的方法。

3.3 fill

在读取数据时,如果缓冲区的数据已经全部读完,则需要将缓冲区的字节重新填装,fill方法就是通过数据滑动的方式实现缓冲区填装,方法的具体源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
  private void fill() throws IOException {

// 获取当前缓冲区数组
byte[] buffer = getBufIfOpen();

// 如果markpos小于0,代表此对象没有调用过mark方法,或者mark后已经被重置过了
if (markpos < 0)
// 将下次读取的坐标设置为0,也就是第一个坐标,具体的最后会讲
pos = 0;

// 如果被mark方法标记,并且下次读取坐标超出缓冲区的范围
else if (pos >= buffer.length)

// 如果mark的缓冲区坐标大于0,调用mark方法后再调用read肯定会进入此方法
// 从mark坐标开始往右的元素都要保存,否则reset后无法从mark坐标重读读取
if (markpos > 0) {
// 计算从缓冲区的最右边开始,往左多少个元素需要保存
int sz = pos - markpos;
// 将buffer右端需要保存的所有字节,滑动到最左端
System.arraycopy(buffer, markpos, buffer, 0, sz);
// 下次读取的坐标仍然从标记后开始
pos = sz;
// 到此mark标记涉及的字节元素都保存下来了,可以将markpos=0
markpos = 0;

// mark后第二次read会进入此if,如果缓冲区足够大,可以支撑marklimit个字节的读取,直接重置
} else if (buffer.length >= marklimit) {
// 直接重置markpos是-1,左滑后设置是0
markpos = -1;
pos = 0;
// 执行到此if判断,说明缓冲区容纳不下marklimit个字节,如果没法扩容只能抛异常
} else if (buffer.length >= MAX_BUFFER_SIZE) {
throw new OutOfMemoryError("Required array size too large");

// 到这里表示缓冲区容纳不下marklimit个元素供重置后读取,但还可以扩容,因此必须扩容
} else {

// 如果2倍扩容后不超过最大容量限制,则进行2倍扩容,否则使用最大容量
int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
pos * 2 : MAX_BUFFER_SIZE;

// 如果计算后的新容量大于marklimit,则使用marklimit
if (nsz > marklimit)
nsz = marklimit;

// 将最终确定的容量大小进行数组的创建
byte nbuf[] = new byte[nsz];

// 将字节数据拷贝到扩容后的缓冲区中
System.arraycopy(buffer, 0, nbuf, 0, pos);

// 使用CAS方式将扩容后的缓冲区赋值到成员变量
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
throw new IOException("Stream closed");
}

// 将扩容后的缓冲区赋值给局部变量
buffer = nbuf;
}

// 先将可读取字节数设置为已读取的数量
count = pos;

// 调用装饰输入流的read方法,向缓冲区填装数据
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);

// 如果从装饰的输入流中读到数据,那么可读字节总数就变成已读+装饰输入流读出的数量
if (n > 0)
count = n + pos;
}

代码有点晦涩难懂,首先缓冲区在没字节可读的情况下才会调用fill(),如果此BufferedInputStream对象没有调用过mark,那么处理起来是最简单的,把下次读取的坐标设置为缓冲区的首坐标(pos=0),从装饰器的输入流中读取缓冲区大小的字节,将缓冲区以覆盖形式填满新字节,下次从头读取。

被mark调用过就比较麻烦,可以看看else if (pos >= buffer.length)执行流程图:
图片

3.3 read(单字节)

1
2
3
4
5
6
7
8
9
10
11
12
13
public synchronized int read() throws IOException {
// count值代表了缓冲区已存字节的数量,pos大于等于此之值,代表缓冲区的数据已经读完了
if (pos >= count) {
// 重新填装
fill();
// 填装后如果还是如此,说明已经读完了,返回-1
if (pos >= count)
return -1;
}

// 根据pos读取缓冲区单个字节,高位补零后返回,pos递增1
return getBufIfOpen()[pos++] & 0xff;
}

3.4 read(多字节)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public synchronized int read(byte b[], int off, int len)
throws IOException
{
// 检查缓冲区是否被关闭
getBufIfOpen();

// 校验参数
if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}

// 根据len长度循环,直到读取len个字节为止
int n = 0;
for (;;) {

// 调用私有方法尽量读
int nread = read1(b, off + n, len - n);

// 如果没读取到直接返回
if (nread <= 0)
return (n == 0) ? nread : n;

// 读取到就将读取的字节数量记录下来
n += nread;

// 读取完成后返回
if (n >= len)
return n;

// 如果装饰的输入流没关闭,但是没字节可以读了,直接返回
InputStream input = in;
if (input != null && input.available() <= 0)
return n;
}
}

私有方法一次读取多个字节(尽量读,有可能填装完还读不满len):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private int read1(byte[] b, int off, int len) throws IOException {

// 计算出缓冲区可读字节中,还未读取的数量
int avail = count - pos;

// 如果没有可读的字节
if (avail <= 0) {

// 如果想要读取的长度大于等于缓冲区长度,并且没有被mark标记
// 缓冲区无法容纳(不扩容情况下),直接从装饰的流中一次性读取,减少数组copy带来的性能消耗
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;
}

4.BufferedOutputStream

缓冲处理输出流相对输入流来说,不用考虑mark后字节的保存情况,也不用考虑缓冲区的扩容情况,唯一要考虑的就是写完后需要手动flush,确保缓冲区数据都刷新到装饰的输入流,因为写入时只有缓冲区满了才会自动刷新。

4.1 成员变量

1
2
3
4
5
6

// 缓冲区
protected byte buf[];

// 已写入字节数量
protected int count;

4.2 write(单字节)

1
2
3
4
5
6
7
8
9
public synchronized void write(int b) throws IOException {
// 如果字节已写满缓冲区
if (count >= buf.length) {
// 刷新缓冲字节(到磁盘等)
flushBuffer();
}
// 将新写入的字节放入缓冲区,count递增1
buf[count++] = (byte)b;
}

调用私有方法,将写入缓冲区的字节数组同步到装饰的输出流:

1
2
3
4
5
6
7
8
9
private void flushBuffer() throws IOException {
// 在缓冲区存在字节的情况下
if (count > 0) {
// 将缓冲区字节写入内部装饰的输出流
out.write(buf, 0, count);
// 缓冲区字节数量归零
count = 0;
}
}

4.3 write(多字节)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public synchronized void write(byte b[], int off, int len) throws IOException {
// 如果要批量写入的字节数量大于缓冲区容量
if (len >= buf.length) {
// 刷新缓冲区
flushBuffer();
// 不会扩容,而是直接写入装饰的输出流
out.write(b, off, len);
// 结束
return;
}

// 如果要批量写入的字节数量大于缓冲区剩余容量,仍然刷新缓冲区
if (len > buf.length - count) {
flushBuffer();
}

// 将要批量写入的字节数组拷贝到缓冲区
System.arraycopy(b, off, buf, count, len);

// 缓冲区字节数量递增1
count += len;
}

评论