Java IO流之字节缓冲流(源码注释)【三】
1 概述
缓冲流,也叫高效流,是对字节流(FileOutputStream
和 FileInputStream
),字符流(FileReader
和FileWriter
)的增强。
缓冲流按照数据类型分类:
-
字节缓冲流:
BufferedOutputStream
,BufferedInputStream
。 -
字符缓冲流:
BufferedWriter
,BufferedReader
。
缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。
2 BufferedOutputStream 【字节缓冲输出流】
java.io.BufferedOutputStream
是字节缓冲输出流。能够将字节存入内部缓冲区,在必要的时候写入底层输出流中,而不必针对每次字节写入调用底层系统,从而减少频繁操作底层系统的次数,能够大幅度提高IO效率。
2.1 BufferedOutputStream 源码注释
package java.io;
/**
* 该类实现缓冲的输出流。
* 通过设置这种输出流,应用程序就可以将各个字节写入底层输出流中,
* 而不必针对每次字节写入调用底层系统。
*/
public class BufferedOutputStream extends FilterOutputStream {
//存储数据的内部缓冲区
protected byte buf[];
// 缓冲区中的有效字节数。大于0,小于buf.length
protected int count;
// 构造缓冲输出流,缓冲区大小为8192(8KB)
public BufferedOutputStream(OutputStream out) {
this(out, 8192);
}
// 构造缓冲输出流,缓冲区大小为传入的int值
public BufferedOutputStream(OutputStream out, int size) {
super(out);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
// 刷新内部缓冲区
private void flushBuffer() throws IOException {
if (count > 0) {
out.write(buf, 0, count);
count = 0;
}
}
// 写入单个字节
public synchronized void write(int b) throws IOException {
if (count >= buf.length) {
flushBuffer();
}
// 实际并没有写入,而是将字节存储在内部缓冲区中
buf[count++] = (byte)b;
}
/**
* 将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此缓冲的输出流。
* 此方法将数组的字节存入缓冲区中,根据需要刷新缓冲区,并转到底层输出流。
* 但是,如果请求的长度超过缓冲区大小,则直接刷新缓冲区并将字节直接写入底层输出流。
*/
public synchronized void write(byte b[], int off, int len) throws IOException {
if (len >= buf.length) {
// 如果请求长度超过输出缓冲区的大小,刷新输出缓冲区,然后直接写入数据。
flushBuffer();
out.write(b, off, len);
return;
}
// 如果内部缓冲的剩余容量,不满足len,则立即刷新缓冲区
if (len > buf.length - count) {
flushBuffer();
}
// 将字节数组复制到缓冲区中,并将字节个数增加相应的数量
System.arraycopy(b, off, buf, count, len);
count += len;
}
// 刷新此缓冲输出流,强制写入缓冲区的字节
public synchronized void flush() throws IOException {
flushBuffer();
out.flush();
}
}
通过BufferedOutputStream
的源码可以得出:
- 默认的构造方法会在内部 新建一个容量为8192的byte[]数组。
- 每次写入字节时,只会将该字节存入内部缓冲区中。
- 只有当缓冲区中的字节个数超过最大容量,才会调用底层操作系统,将缓冲区中的全部字节写入。
3 BufferedInputStream【字节缓冲输入流】
java.io.BufferedInputStream
为字节流添加新功能,即缓冲输入、支持mark
和reset
方法。
在创建 BufferedInputStream 时,会创建一个内部缓冲区数组。
在读取或跳过流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。
mark 操作记录输入流中的某个点,reset 操作使得在从包含的输入流中获取新字节之前,再次读取自最后一次 mark 操作后读取的所有字节。
3 .1 BufferedInputStream源码注释
package java.io;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* 为字节流添加了新的功能,即缓冲输入以及支持 mark 和 reset 方法的能力。
* 在创建 BufferedInputStream 时,会创建一个内部缓冲区数组。
* 在读取或跳过流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。
* mark 操作记录输入流中的某个点,
* reset 操作使得在从包含的输入流中获取新字节之前,再次读取自最后一次 mark 操作后读取的所有字节。
*/
public class BufferedInputStream extends FilterInputStream {
// 默认缓冲区大小(8KB)
private static int DEFAULT_BUFFER_SIZE = 8192;
// 可分配的最大数组大小
private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
// 存储数据的内部缓冲区数组(线程共享),必要时可用另一个不同大小的数组替换它。
protected volatile byte buf[];
// 为buf[] 提供原子操作,以防该流对象被异步关闭。如果流对象被关闭,那么buf[]为null
private static final
AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
AtomicReferenceFieldUpdater.newUpdater
(BufferedInputStream.class, byte[].class, "buf");
// 缓冲区的字节个数
protected int count;
/**
* 缓冲区中的当前位置,是下一个从buf数组中读取的字节的索引。
* 此值始终处于 0 到 count 的范围内。如果此值小于 count,则 buf[pos] 将作为下一个输入字节;
* 如果此值等于 count,则下一次 read 或 skip 操作需要从包含的输入流中读取更多的字节。
*/
protected int pos;
/**
* 上一次调用 mark 方法时 pos 字段的值。
* 如果输入流中有被标记的位置,则buf[markpos]将用作reset操作后的第一个输入字节。
* 若markpos不是 -1,则从位置buf[markpos]到 buf[pos-1]之间的所有字节都必须保留在缓冲区中
* (尽管对 count、pos 和 markpos 的值进行适当调整后,字节可能移动到缓冲区中的其他位置
* 除非 pos 与 markpos 的差超过 marklimit,否则不能将其丢弃。
*/
protected int markpos = -1;
/**
* 调用 mark 方法后,在后续调用 reset 方法失败之前所允许的最大提前读取量。
* 只要 pos 与 markpos 之差超过 marklimit,就可以通过将 markpos 设置为 -1 来删除该标记。
*/
protected int marklimit;
// 如果底层输入流没有被关闭,就返回该流对象
private InputStream getInIfOpen() throws IOException {
InputStream input = in;
if (input == null)
throw new IOException("Stream closed");
return input;
}
// 如果buf数组没有被关闭,就返回该数组
private byte[] getBufIfOpen() throws IOException {
byte[] buffer = buf;
if (buffer == null)
throw new IOException("Stream closed");
return buffer;
}
// 使用默认大小创建流对象
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];
}
// 将字节存入缓冲区
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos < 0)
// 没有标记,将当前位置设为0
pos = 0;
else if (pos >= buffer.length) // 有标记,且buffer数组的空间已满,进行扩容
if (markpos > 0) { // 丢弃上一次标记位置之前的数据
int sz = pos - markpos;
// 将上一次标记的位置(markpos),至当前位置之间的字节复制到buffer数组中。
// 从markpos的位置开始,将buffer的数组中的内容,复制到自身,一共复制sz个字节
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) {
// 缓冲区过大,标记无效,并删除缓冲区的内容
markpos = -1;
pos = 0;
} else if (buffer.length >= MAX_BUFFER_SIZE) {
// 数组长度过大,内存溢出。
throw new OutOfMemoryError("Required array size too large");
} else {// 缓冲区扩容
// 如果扩容后的容量不大于MAX_BUFFER_SIZE,就按当前位置2倍扩容
int nsz = (pos <= MAX_BUFFER_SIZE - pos) ? pos * 2 : MAX_BUFFER_SIZE;
if (nsz > marklimit)
// 如果扩容后的容量大于可预读数,就将扩容后的容量设为可预读数(一次性装满即可,高效!)
nsz = marklimit;
byte nbuf[] = new byte[nsz];
// 把buffer数组o~pos位置的数据复制到nbuf中
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
/**
* 如果发生异步关闭,则无法替换buf。
*/
throw new IOException("Stream closed");
}
buffer = nbuf;
}
// 新数组中只有pos个字节
count = pos;
// 往流中读取
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
}
// 返回读取到的下一个数据字节,如果到达流末尾,则返回 -1。
public synchronized int read() throws IOException {
// 若读取位置超过容量,进行填充缓冲区
if (pos >= count) { //当前读取位置大于缓冲区容量,进行容量,正常情况是2倍pos的容量
// 填充缓冲区。一次性读取buff.length个字节
fill();
if (pos >= count)
return -1;
}
/**
* &表示按位与,只有两个位同时为1,才能得到1,。
* 0x代表16进制数,0xff表示的数为二进制1111 1111 占一个字节。
* 1. 和0xff进行&操作的数,最低8位,不会发生变化。
* 2. 补码一致,将有符号数转为无符号数
* 推荐博客:https://blog.csdn.net/i6223671/article/details/88924481
*/
return getBufIfOpen()[pos++] & 0xff;
}
// 读取方法
private int read1(byte[] b, int off, int len) throws IOException {
// 获取缓冲区剩余容量
int avail = count - pos;
if (avail <= 0) {
/**
* 如果请求的长度大于或者等于缓冲区大小,并且没有mark/reset活动。
* 无需将字节放入本地缓冲区。以保证缓冲流的可用性(如果将这些字节放入本地缓冲区,将会导致扩容问题)。
*/
if (len >= getBufIfOpen().length && markpos < 0) {
return getInIfOpen().read(b, off, len);
}
// 将字节存入缓冲区
fill();
avail = count - pos;
// 流末尾,返回-1
if (avail <= 0) return -1;
}
// 获取实际读取到的字节个数
int cnt = (avail < len) ? avail : len;
// 将缓冲区从当前位置的pos复制到b数组中,从off开始,复制cnt个
System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
pos += cnt;
// 返回实际读取到的字节个数
return cnt;
}
/**
* 读取 byte 数组的一部分。
* 它将通过重复调用底层流的 read 方法,尝试读取尽可能多的字节。
* 这种迭代的 read 会一直继续下去,直到满足以下条件之一:
* 1.已经读取了指定的字节数,
* 2.底层流的 read 方法返回 -1,指示文件末尾(end-of-file),或者
* 3.底层流的 available 方法返回 0,指示将阻塞后续的输入请求。
* 如果第一次对底层调用read 返回 -1(文件末尾),则返回 -1。 否则返回实际读取的字节数。
* 鼓励(但不是必须)此类的各个子类以相同的方式尝试读取尽可能多的字节。
*/
public synchronized int read(byte b[], int off, int len) throws IOException{
// 验证流是否被关闭
getBufIfOpen();
// 按位或运算,规则:0|0=0; 0|1=1; 1|0=1; 1|1=1
if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
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;
}
}
// 跳过字节
public synchronized long skip(long n) throws IOException {
// 验证流是否关闭
getBufIfOpen();
if (n <= 0) {
return 0;
}
// 获取剩余待读取字节的个数
long avail = count - pos;
if (avail <= 0) { //没有可用容量。
// 如果没有标记的位置,直接跳过指定的字节数
if (markpos <0)
return getInIfOpen().skip(n);
// 如果有标记的位置,那么将字节填充至缓冲区,以便后续调用重置方法reset()
fill();
avail = count - pos;
if (avail <= 0)
return 0;
}
// 获取实际跳过的字节数
long skipped = (avail < n) ? avail : n;
// 更改缓存区的当前位置
pos += skipped;
return skipped;
}
/**
* 返回可以从此输入流读取(或跳过)、且不受此输入流接下来的方法调用阻塞的估计字节数。
* 接下来的调用可能是同一个线程,也可能是不同线程。一次读取或跳过这么多字节将不会受阻塞,但可以读取或跳过数量更少的字节。
* 此方法返回缓冲区中剩余的待读取字节数 (count - pos) 与调用 in.available() 的结果之和。
*/
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;
}
/**
* 在输入流中的当前位置上作标记。
* reset 方法的后续调用将此流重新定位在最后标记的位置上,以便后续读取操作重新读取相同的字节。
* readlimit 参数告知此输入流在标记位置无效之前允许读取的字节数。
*/
public synchronized void mark(int readlimit) {
marklimit = readlimit;
markpos = pos;
}
/**
* 将此流重新定位到对此输入流最后调用 mark 方法时的位置。
* 如果 markpos 为 -1(尚未设置标记,或者标记已失效),则抛出 IOException。否则将 pos 设置为与 markpos 相等。
* 在需要提前读取一小部分数据以查看流中有什么的情况下,可以使用流的标记。通过调用通用解析器常常最容易做到这一点。
* 如果流属于通过解析处理的类型,那么解析起来就很容易。
* 如果流不属于那种类型,那么解析器应该在解析失败时抛出一个异常。
* 如果这发生在 readlimit 个字节内,那么它允许外部代码重置流,并尝试另一种解析器。
*/
public synchronized void reset() throws IOException {
getBufIfOpen();
if (markpos < 0)
throw new IOException("Resetting to invalid mark");
pos = markpos;
}
/**
* 测试此输入流是否支持 mark 和 reset 方法。
* BufferedInputStream 的 markSupported 方法返回 true。
*/
public boolean markSupported() {
return true;
}
/**
* 关闭此输入流并释放与该流关联的所有系统资源。
* 关闭该流之后,后续的 read()、available()、reset() 或 skip() 调用都将抛出 IOException。
* 关闭之前已关闭的流不会产生任何效果。
*/
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()
}
}
}
通过阅读BufferedInputStream
的源码可以得知:
- 尽管
read()
方法只读取一个字节,也会一次性尝试从底层阅读足以填充整个缓冲区的字节数量。 - 在一些特定的情况,内部缓冲区会进行扩容,正常情况下会扩容至两倍当前位置的容量。
- 内部缓冲区被protected修饰,无法直接访问,所以一般情况下,会自定义数组,通过方法,将缓冲区中的内容读取至自定义的数组中。
4 文件复制验证效率
将 D:\葫芦小金刚.3gp 进行复制,该文件大小为10M。
代码演示:
package com.hanyxx.io;
import java.io.*;
/**
* 验证字节缓冲流的效率(文件大小为10M)
* @author layman
*/
public class Demo08 {
public static void main(String[] args) throws IOException {
//testInputAndOut();
testBufferInputAndOut();
}
// 测试普通字节流的效率
private static void testInputAndOut() throws IOException {
FileInputStream fis = new FileInputStream("D:\\葫芦小金刚.3gp");
FileOutputStream fos = new FileOutputStream("D:\\葫芦小金刚_copy.3gp");
long Start = System.currentTimeMillis();
int len;
while((len = fis.read())!=-1){
fos.write(len);
}
long end = System.currentTimeMillis();
// 普通字节流复制文件,耗时:58468毫秒
System.out.println("普通字节流复制文件,耗时:" + (end - Start) + "毫秒");
}
//测试字节缓冲流的效率
private static void testBufferInputAndOut() throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\葫芦小金刚.3gp"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\葫芦小金刚_copy1.3gp"));
long Start = System.currentTimeMillis();
int len;
byte[] bytes = new byte[1024];
while((len =bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
long end = System.currentTimeMillis();
//字节缓冲流复制文件,耗时:55毫秒
System.out.println("字节缓冲流复制文件,耗时:" + (end - Start) + "毫秒");
}
}
运行结果:
普通字节流复制文件,耗时:58468毫秒
字节缓冲流复制文件,耗时:55毫秒
看到结果的我,忍不住竖起了大拇指:
缓冲流牛逼!