java input/output速度对比
说明
本篇文章讨论自定义数组大小,对不同IO读写方法速度的影响
读写速度受计算机软硬件环境影响,测试结果为多次测试数据,去除最大值和最小值,求平均值得出。
继承关系
- java.lang.Object
- java.io.OutputStream
- java.io.FilterOutputStream
- java.io.BufferedOutputStream
- java.io.FilterOutputStream
- java.io.InputStream
- java.io.FilterInputStream
- java.io.BufferedInputStream
- java.io.FilterInputStream
- java.io.Reader
- java.io.BufferedReader
- java.io.InputStreamReader
- java.io.FileReader
- java.io.Writer
- java.io.BufferedWriter
- java.io.OutputStreamWriter
- java.io.FileWriter
- java.io.OutputStream
生成测试文件
BufferedWriter bw = new BufferedWriter(fw);
String str = "从前有座,山上有座庙,庙里有个老和尚,在和小和尚讲故事,讲的故事是:";
for (int i = 0; i < 10000000; i++) {
fw.write(str);
}
测试文本大小:972MB
字节流
代码
public static void main(String[] args) {
File file = new File("src/io/a.txt");
File file1 = new File("src/io/test.txt");
if (!file.exists()) {
System.out.println("file is not found");
return;
}
long start = System.currentTimeMillis();
try {
InputStream bis = new FileInputStream(file);
OutputStream bos = new FileOutputStream(file1);
// InputStream ins = new FileInputStream(file);
// BufferedInputStream bis = new BufferedInputStream(ins);
// OutputStream fos = new FileOutputStream(file1);
// BufferedOutputStream bos = new BufferedOutputStream(fos);
// 每次写入byte[1024]大的数据,其实这里就使用了缓存的思想
byte[] bytes = new byte[1024];
int len = -1 ;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
// BufferedOutputStream的close方法,将关闭此输入流并释放与流相关联的 任何 系统资源。
bis.close();
bos.close();
// 上边已经能关闭了流,下面这句会抛出IOException: Stream Closed
// System.out.println(ins.read());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
结果
byte[]长度 | FileInputStream FileOutputStream |
BufferedInputStream BufferedOutputStream |
---|---|---|
1024 | 7953 ms | 5840 ms |
8192 | 5193 ms | 5458 ms |
102400 | 4820 ms | 4904 ms |
结论
BufferedOutputStream默认缓冲区是一个 byte[8192] 的数组。
每次写入数据,如果数据小于8k,数据将保存到缓冲 buf 中,不进行磁盘io,一直到缓冲 buf 满了之后,才进行一次磁盘IO,把缓冲buf的字节全部写入磁盘。
如果数据大于8K,先把刷新缓冲区,把缓冲区内容写入到磁盘,然后把传入的数据写入到磁盘。(此过程两次IO)
如果每次传入数据大于等于8k,使用BufferedOutputStream的效率反而会慢,因为每次写入数据都多了一步——刷新缓存区
BufferedOutputStream.class 部分源码:
// 存储数据的内部缓冲区
protected byte buf[];
// 创建一个新的缓冲输出流来将数据写入指定底层输出流
public BufferedOutputStream(OutputStream out) {
// 这里将调用下面的 BufferedOutputStream 构造方法
this(out, 8192);
}
// 创建一个新的缓冲输出流,来将数据写入,使用指定的缓冲区,指定底层输出流大小。
public BufferedOutputStream(OutputStream out, int size) {
super(out);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
// 指定 buf 数组的大小
buf = new byte[size];
}
/**
*该方法将给定数组中的字节存储到这个数组流的缓冲区中,将刷新缓冲区到底层输出流
*如果请求的长度大于等于此流的长度,该方法将刷新缓冲区并直接把字节写入到底层输出流。
*/
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;
}
/** 刷新内部缓冲区 */
private void flushBuffer() throws IOException {
// 如果缓存区中有数据,count:缓冲区中有效字节数
if (count > 0) {
out.write(buf, 0, count);
count = 0;
}
}
字符流
代码
public static void main(String[] args) {
File file = new File("src/io/a.txt");
File file1 = new File("src/io/test.txt");
if (!file.exists()) {
System.out.println("file is not found");
return;
}
long start = System.currentTimeMillis();
try {
Reader br = new FileReader(file);
Writer bw = new FileWriter(file1);
// BufferedReader br = new BufferedReader(fr);
// BufferedWriter bw = new BufferedWriter(fw);
// 这里是字符数组
char[] ch = new char[1024];
int len = -1;
while ((len = br.read(ch)) != -1) {
bw.write(ch, 0, len);
}
bw.close();
br.close();
long end = System.currentTimeMillis();
System.out.println("time:" + (end - start));
} catch (IOException e) {
e.printStackTrace();
}
}
结果
char[]长度 | FileReader FileWriter |
BufferedReader BufferedWriter |
---|---|---|
1024 | 7153 ms | 7056 ms |
8192 | 7067 ms | 7254 ms |
102400 | 6826 ms | 6979 ms |
结论
FileWriter本来是没有缓存区的;
API中说它有缓存,也是有原因的:
FileWriter流中的构造函数调用的是父类OutputStreamWriter构造函数;
而父类OutputStreamWriter构造函数本质是StreamEncoder类的forOutputStreamWriter方法。StreamEncoder中是有缓存区的。FileWriter执行写操作时,会调用父类OutputStreamWriter的方法,OutputStreamWriter的write方法在Writer类中;而Writer类的write方法是被StreamEncoder类实现的。
调用过程:
FileWriter --> OutputStreamWriter --> Writer --> StreamEncoder
构造函数相互调用代码FileWriter extends OutputStreamWriter
// FileWriter 构造方法
public FileWriter(File file) throws IOException {
super(new FileOutputStream(file));
}
// OutputStreamWriter 构造方法
public OutputStreamWriter(OutputStream out) {
super(out);
try {
se = StreamEncoder.forOutputStreamWriter(out, this, (String)null);
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
FileWriter中没有任何实现方法,直接看他的父类
OutputStreamWriter.class 部分源码:
// StreamEncoder 字节字符转换流
private final StreamEncoder se;
/** 写入字符数组 */
public void write(char cbuf[], int off, int len) throws IOException {
// 这里调用了se的write方法,看下面StreamEncoder的源码
se.write(cbuf, off, len);
}
StreamEncoder 部分源码:
// 是否保存左字符标志,为了保证读入的字符不乱码 则每次读入不能少于两个字符
private boolean haveLeftoverChar = false;
// 默认字节缓冲区大小
private static final int DEFAULT_BYTE_BUFFER_SIZE = 8192;
// 字节缓冲区
private ByteBuffer bb;
void implWrite(char cbuf[], int off, int len) throws IOException {
// 将字符序列包装到缓冲区中
// 缓冲区的容量将为 cbuf.length(),其位置将为 off,其限额将为 len
CharBuffer cb = CharBuffer.wrap(cbuf, off, len);
// 为了保证读入的字符不乱码 则每次读入不能少于两个字符
if (haveLeftoverChar)
flushLeftoverChar(cb, false);
// hasRemaining()方法用于判断字符缓冲区中是否还有元素
while (cb.hasRemaining()) {
// 从给定的缓冲区对象中,编码尽可能多的字符,把结果(字节)写入给定的输出缓冲区,并返回终止原因。
// 参数false代表有可能提供其他输入
// bb: 字节缓存区,private ByteBuffer bb;
CoderResult cr = encoder.encode(cb, bb, false);
if (cr.isUnderflow()) {
assert (cb.remaining() <= 1) : cb.remaining();
if (cb.remaining() == 1) {
haveLeftoverChar = true;
leftoverChar = cb.get();
}
break;
}
if (cr.isOverflow()) {
assert bb.position() > 0;
// 利用OutputStreamWriter对象,所使用到的底层字节输出流FileOutputStream,
// 把字节缓冲区的内容给输出出去,然后清空字节流
// 这里不再继续往下扒源码
writeBytes();
continue;
}
cr.throwException();
}
}
public void write(char cbuf[], int off, int len) throws IOException {
synchronized (lock) {
// 确保流是打开的状态
ensureOpen();
if ((off < 0) || (off > cbuf.length) || (len < 0) ||
((off + len) > cbuf.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
implWrite(cbuf, off, len);
}
}
BufferedWriter缓冲区是一个 char[8192] 数组。
每次写入数据,如果数据小于8192个字符,数据将保存到缓冲 cb 中,不进行编码转换,一直到缓冲 cb 满了之后,才进行一次编码转换。
如果数据大于8192个字符,先把刷新缓冲区,如果缓存区有内容,就把缓冲区内容进行一次编码转换,然后把传入的数据再进行编码转换。
编码转换 StreamEncoder 类中也有缓存区,大小为8192。
调用过程:
BufferedWriter --> Writer --> StreamEncoder
// BufferedWriter.class
private char cb[];
private static int defaultCharBufferSize = 8192;
public void write(char cbuf[], int off, int len) throws IOException {
synchronized (lock) {
ensureOpen();
if ((off < 0) || (off > cbuf.length) || (len < 0) ||
((off + len) > cbuf.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
if (len >= nChars) {
// 如果请求长度超过输出缓冲区的大小,刷新缓冲区,然后直接写入数据
flushBuffer();
// 这里的 write
out.write(cbuf, off, len);
return;
}
int b = off, t = off + len;
while (b < t) {
int d = min(nChars - nextChar, t - b);
System.arraycopy(cbuf, b, cb, nextChar, d);
b += d;
nextChar += d;
if (nextChar >= nChars)
flushBuffer();
}
}
}
结论
对于BufferedWriter和FileWriter在写入数据时,要进行的编码转换,在对比速度时可以忽略,因为二者都有这一步。
如果自定义缓存大小(char[] / byte[]),大于 BufferedXxxx() 方法,默认的缓存大小(8192),使用 BufferedXxxx() 方法,速度反而会慢,因为多了一步刷新缓存的步骤。