带缓冲的输入输出字节流
- BufferedInputStream
- BufferedOutputStream
BufferedInputStream
首先解释一下BufferedInputStream的基本原理:
在创建 BufferedInputStream时,会创建一个内部缓冲区数组。在读取流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。
也就是说,BufferedInputStream类初始化时会创建一个较大的byte数组,一次性从底层输入流中读取多个字节来填充byte数组,当程序读取一个或多个字节时,可直接从byte数组中获取,当内存中的byte读取完后,会再次用底层输入流填充缓冲区数组。这种从直接内存中读取数据的方式要比每次都访问磁盘的效率高很多。
我们来看BufferedInputStream源码分析:
public class BufferedInputStream extends FilterInputStream {
/**
* 缓冲区数组默认大小8192字节,也就是8K。
* BufferedInputStream 会根据“缓冲区大小”来逐次的填充缓冲区;
* 即,BufferedInputStream 填充缓冲区,用户读取缓冲区,
* 读完之后,BufferedInputStream 会再次填充缓冲区。
* 如此循环,直到读完数据...
*/
private static int defaultBufferSize = 8192;
/**
* 内部缓冲数组,会根据需要进行填充。
* 大小默认为8192字节,也可以用构造函数自定义大小
*/
protected volatile byte buf[];
/**
* 缓存数组的原子更新器。
* 该成员变量与buf数组的volatile关键字共同组成了buf数组的原子更新功能实现,
* 即,在多线程中操作BufferedInputStream对象时,buf和bufUpdater都具有原子性(不同的线程访问到的数据都是相同的)
*/
private static final AtomicReferenceFieldUpdater<bufferedinputstream byte=""> bufUpdater = AtomicReferenceFieldUpdater
.newUpdater(BufferedInputStream.class, byte[].class, "buf");
/**
* 缓冲区中还没有读取的字节数。当count=0时,说明缓冲区内容已读完,会再次填充。
* 注意,这里是指缓冲区的有效字节数,而不是输入流中的有效字节数。
*/
protected int count;
/**
* 缓冲区指针,记录缓冲区当前读取到的位置
* 注意,这里是指缓冲区的位置索引,而不是输入流中的位置索引。
*/
protected int pos;
/**
* 当前缓冲区的标记位置
* markpos和reset()配合使用才有意义。操作步骤:
* (01) 通过mark() 函数,保存pos的值到markpos中。
* (02) 通过reset() 函数,会将pos的值重置为markpos。
* 接着通过read()读取数据时,就会从mark()保存的位置开始读取。
*/
protected int markpos = -1;
/**
* marklimit是标记的最大值。
* 关于marklimit的原理,在后面的fill()函数分析中说明。这对理解BufferedInputStream相当重要。
*/
protected int marklimit;
/**
* 真正读取字节的还是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
*
* @param in
* 底层字节流对象
*/
public BufferedInputStream(InputStream in) {
this(in, defaultBufferSize);
}
/**
* 此构造方法可以自定义缓冲区大小
*
* @param in
* 底层字节流对象
* @param size
* 自定义缓冲区大小
*/
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
/**
* 填充缓冲区数组的具体实现算法,通过各if-else语句,分5种情形讨论。
* 情形1:读取完buffer中的数据,并且buffer没有被标记。
* 输入流中有很长的数据,我们每次从中读取一部分数据到buffer中进行操作。
* 每次当我们读取完buffer中的数据之后,并且此时输入流没有被标记;
* 那么,就接着从输入流中读取下一部分的数据到buffer中。
* 情形2:读取完buffer中的数据,buffer的标记位置>0,并且buffer中没有多余的空间。
* 输入流中有很长的数据,我们每次从中读取一部分数据到buffer中进行操作。
* 当我们读取完buffer中的数据之后,并且此时输入流存在标记时,那么,就发生情况2。此时,
* 我们要保留“被标记位置”到“buffer末尾”的数据,然后再从输入流中读取下一部分的数据到buffer中。
* 情形3:读取完buffer中的数据,buffer被标记位置=0,buffer中没有多余的空间,并且buffer.length>=marklimit。
* 情形4:读取完buffer中的数据,buffer被标记位置=0,buffer中没有多余的空间,并且buffer.length<marklimit 5="" buffer="" marklimit="" markpos="" 4="">=marklimit时,就不再保存markpos的值了。
*/
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos < 0) // 情形1:输入流是否被标记,若被标记,则markpos>=0,否则,markpos=-1
pos = 0; /* no mark: throw away the buffer */
else if (pos >= buffer.length) // buffer中没有多余的空间
if (markpos > 0) { // 情形2:标记位置大于0
int sz = pos - markpos; // 获取“‘被标记位置’到‘buffer末尾’”的数据长度
System.arraycopy(buffer, markpos, buffer, 0, sz); // 将buffer中从markpos开始的数据”拷贝到buffer中
pos = sz; // 将sz赋值给pos,即pos就是“被标记位置”到“buffer末尾”的数据长度
markpos = 0; // 标记位置置0
} else if (buffer.length >= marklimit) { // 情形3:
markpos = -1; // 取消标记
pos = 0; // 设置初始化位置为0
} else { // 情形4:
// 新建一个数组nbuf,其大小是pos*2与marklimit中较小值
int nsz = pos * 2;
if (nsz > marklimit)
nsz = marklimit;
byte nbuf[] = new byte[nsz];
// 将buffer中的数据拷贝到新数组nbuf中
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
throw new IOException("Stream closed");
}
buffer = nbuf;
}
count = pos;
// 从输入流中读取出“buffer.length - pos”的数据,然后填充到buffer中
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos; // 根据从输入流中读取的实际数据的多少,来更新buffer中数据的实际大小
}
/**
* 读取下一个字节。
* 读取下一个字节 与FileInputStream中的read()方法不同的是,这里是从缓冲区数组中读取了一个字节
* 也就是直接从内存中获取的,效率远高于前者
*
* @return
*/
public synchronized int read() throws IOException {
if (pos >= count) { // 是否读完buf中的数据
// 若已经读完缓冲区中的数据,则调用fill()从输入流读取下一部分数据来填充缓冲区
fill();
if (pos >= count)
return -1;
}
// 从缓冲区中读取指定的字节并返回
return getBufIfOpen()[pos++] & 0xff;
}
/**
* 将缓冲区中的数据写入到字节数组b中。off是字节数组b的起始位置,len是写入长度
*
* @param b
* @param off
* 起始位置
* @param len
* 读取长度
* @return
* @throws IOException
*/
private int read1(byte[] b, int off, int len) throws IOException {
int avail = count - pos;
if (avail <= 0) {
// 加速机制。
// 如果读取的长度大于缓冲区的长度,并且没有markpos, 则直接从原始输入流中进行读取并返回,
// 从而避免无谓的缓冲区数据填充
if (len >= getBufIfOpen().length && markpos < 0) {
return getInIfOpen().read(b, off, len);
}
// 若已经读完缓冲区中的数据,则调用fill()从输入流读取下一部分数据来填充缓冲区
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;
}
/**
* 将缓冲区中的数据写入到字节数组b中。off是字节数组b的起始位置,len是写入长度
*
* @param b
* @param off
* 起始位置
* @param len
* 读取长度
* @return
* @throws IOException
*/
public synchronized int read(byte b[], int off, int len) throws IOException {
getBufIfOpen(); // Check for closed stream
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;
// if not closed but no bytes available, return
InputStream input = in;
if (input != null && input.available() <= 0)
return n;
}
}
/**
* 跳过和丢弃此输入流中数据的 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;
}
/**
* 返回下一次对此输入流调用的方法可以不受阻塞地从此输入流读取(或跳过)的估计剩余字节数
*
* @return 可以不受阻塞地从此输入流中读取(或跳过)的估计字节数
*/
public synchronized int available() throws IOException {
return getInIfOpen().available() + (count - pos);
}
/**
* 标记“缓冲区”中当前位置,设定marklimit值。
*/
public synchronized void mark(int readlimit) {
marklimit = readlimit;
markpos = 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;
}
public boolean markSupported() {
return true;
}
/**
* 关闭输入流
*/
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()
}
}
}
</marklimit></bufferedinputstream>
BufferedOutputStream
BufferedOutputStream实现了一个缓冲输出流。构建了这样一个输出流后,应用可以往底层流写数据而不用每次写一个字节都调用底层流的方法
下面我们来分析一下BufferedOutputStream的源码:
public class BufferedOutputStream extends FilterOutputStream {
/**
* 内部缓冲区,存储数据
*/
protected byte buf[];
/**
* 缓冲区中有效的字节数(0 ~ buf.length)
*/
protected int count;
/**
* 构造方法,创建一个新的输出缓冲流,并把数据写到指定的底层输出流
*
* @param out
*/
public BufferedOutputStream(OutputStream out) {
this(out, 8192);
}
/**
* 构造方法,创建一个新的输出缓冲流,以将具有指定缓冲区大小的数据写到指定的底层输出流。
*
* @param out
* @param size
*/
public BufferedOutputStream(OutputStream out, int size) {
super(out);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
/**
* 刷新内部的缓冲区,这会让内部缓冲区的有效字节被写出到此缓冲的输出流中
*
* @throws IOException
*/
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;
}
/**
* 将指定字节数组中的从偏移量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;
}
if (len > buf.length - count) {
flushBuffer();
}
System.arraycopy(b, off, buf, count, len);
count += len;
}
/**
* 刷新此缓冲区和输出流,这会使所有缓冲的输出字节被写出到底层输出流中。
* 为了让缓冲区的数据能被写入到底层输出流中,可以显式调用该方法。
* 或者调用close()方法(父类FilterOutputStream的close()方法),在close方法里,调用该flush()方法
*/
public synchronized void flush() throws IOException {
flushBuffer();
out.flush();
}
}
示例
复制一个文件,比较使用BufferedInputStream/BufferedOutputStream与FileInputStream/FileOutputStream的效率
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* 复制文件,效率测试
*
* @author 小明
*
*/
public class BufferedTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
copyByBuffer("E:\\jdk-6u23-windows-i586.exe", "d:\\test.exe");
long end = System.currentTimeMillis();
System.out.println("缓冲:" + (end - start));
System.out.println("***************");
start = System.currentTimeMillis();
copy("E:\\jdk-6u23-windows-i586.exe", "d:\\test2.exe");
end = System.currentTimeMillis();
System.out.println("不带缓冲:" + (end - start));
}
/**
* 文件复制
*
* @param source
* 源文件
* @param destination
* 目标文件
*/
private static void copy(String source, String destination) {
InputStream in = null;
OutputStream out = null;
File file = new File(destination);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
try {
// 打开流
in = new FileInputStream(source);
out = new FileOutputStream(file);
// 操作:读写
int len;
while (-1 != (len = in.read())) {
out.write(len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 释放资源
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 文件复制,带缓冲字节流
*
* @param source
* 源文件
* @param destination
* 目标文件
*/
private static void copyByBuffer(String source, String destination) {
BufferedInputStream in = null;
BufferedOutputStream out = null;
try {
// 打开流
in = new BufferedInputStream(new FileInputStream(source));
out = new BufferedOutputStream(new FileOutputStream(destination));
// 读写
int len;
while (-1 != (len = in.read())) {
out.write(len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 释放资源
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
缓冲:90 *************** 不带缓冲:16506
由此可见,使用带缓冲的输入输出流,效率会明显地得到提升。