作为操作文件IO流最基本的类,应该是每个学习Java的人必知必会的。以前只是会用,不能理解什么是流,不能理解为什么用完必须要close(),更不能理解为什么用了BufferInputStream、BufferOutputStream后速度更快一点,对于这一点,虽然网上很多博客都说这是Buffer包装流带来的功能,但是真相真的是这样吗?
本章将会揭开这一系列问题神秘的面纱,寻找真相。
在BIO体系中,可以分为节点流和处理流,节点流是指从一个节点开始流向另一个地方的流,在我的理解当中,节点流是一个流对象最初的形态。FileInputStream和FileOutputStream作为操作文件的节点流,它们代表了不同的流向,除此以外,其他无太大区别。
以FileInputStream为例,本章解析它的源码,结合操作系统知识来分析整个过程。
FileInputStream继承了InputStream接口,实现了接口的方法。除此之外,FileInputStream持有成员变量看一下:
/* File Descriptor - handle to the open file */
private final FileDescriptor fd;
private final String path;
private FileChannel channel = null;
private final Object closeLock = new Object();
private volatile boolean closed = false;
从持有成员变量中可以看出有一个FileDescriptor对象,FileDescriptor在什么地方使用的?看一下构造器:
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.attach(this);
path = name;
open(name);
}
在构造器中可以看到 fd.attach(this); open(name);
其实就是打开了系统文件句柄。系统文件句柄属于一种有限的系统资源,
所以当我们用完流以后必须要close,防止系统句柄泄漏。
public void close() throws IOException {
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}
if (channel != null) {
channel.close();
}
fd.closeAll(new Closeable() {
public void close() throws IOException {
close0();
}
});
}
FileInputStream的close方法重写了父类close,其中对FileDescriptor进行了关闭。
path是该文件流的路径 , FileChannel channel是对应NIO体系中的文件操作,这个在NIO体系中再介绍。
这个类结构很简单,其作用就是将对应的文件操作打开,并以流的方式准备就绪。
这个类的重点就是read操作,以及不同read方法对应的底层实现,效率对比。
第一种read:
public int read()
第二种read:
public int read(byte b[])
public int read(byte b[], int off, int len)
由于后一种read都是调用的同一个方法,所以归为一类。
在研究这两种read之前,先搜一下网上关于BufferInputStream是否可以提高效率这个问题,很多博客都是千篇一律的说BufferInputStream可以提高效率,图文并茂,卡车运货、坐飞机等等例子形象生动,许多学习的人也亲自动手试了一下,果然真的可以提高效率,但事实的真相是什么呢?
看一下BufferInputStream:
BufferInputStream是FilterInputStream的子类,是一个装饰器模式下的包装流实现类。
在BufferInputStream类中看一下它的read方法:
第一种read及调用方法:
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
private byte[] getBufIfOpen() throws IOException {
byte[] buffer = buf;
if (buffer == null)
throw new IOException("Stream closed");
return buffer;
}
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;
}
解析: 单个字节read的读取,其实是优先读取BufferInputStream中缓冲区的数据。缓冲区为
private static int DEFAULT_BUFFER_SIZE = 8192;
如果缓冲区命中,则取缓存中的数据,如果缓存区数据读完了,在fill()方法中,它调用了这一句:
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
也就是说,当缓存命中失败,会再一次调用节点流自己的public int read(byte b[], int off, int len)方法将数据加载到BufferInputStream的缓存区中。
第二种read及调用方法:
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;
}
}
private int read1(byte[] b, int off, int len) throws IOException {
int avail = count - pos;
if (avail <= 0) {
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;
}
在第二种批量字节read中,它通过调用read1方法,在read1方法中不是执行fill()方法,就是执行 return getInIfOpen().read(b, off, len);
但BufferInputStream能够提高IO性能取决于节点流的public int read(byte b[], int off, int len)方法,而这个方法来自抽象父类InputStream。在InputStream中,这个方法是这样实现的:
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
也就是说这个方法的默认实现其实是通过循环调用的单字节read()方法。
如果一个原始节点流在没有重写来自父类的public int read(byte b[], int off, int len)时,使用BufferInputStream相当于在循环中不断调用单字节read(),并不能提高性能。
那为什么FileInputStream用了BufferInputStream包装以后,性能上可以有较大提升呢?
回到上面FileInputStream的这两种read有什么不同。
第一种read:
public int read() throws IOException {
return read0();
}
private native int read0() throws IOException;
这个方法继承自父类InputStream,同时重写了该方法,调用了本地方法read0();
read0()的执行过程是怎样的?
如果要请求更多的字节,还得这样走一遍。所以这是很消耗系统资源的操作。
第二种read:
public int read(byte b[]) throws IOException {
return readBytes(b, 0, b.length);
}
private native int readBytes(byte b[], int off, int len) throws IOException;
这个方法重写并调用了本地方法readBytes();与read0方法不同的是,该方法以子阵列的方式获取数据,也就是批量获取。同样的也要执行read0对应的过程才能取回数据,不过执行一次就能获取批量数据,系统资源消耗上要节省不少。
因此,基于FileInputStream的两种read方法调用的本地方法的不同实现,产生了性能上的差异。当采用BufferInputStream操作FileInputStream时,触发了这种性能差异。
所以循环调用FileInputStream的read和循环调用BufferInputStream包装后的read,性能差异产生的原因在于FileInputStream对两种read实现上的不同。而不是BufferInputStream带来的功能。
如果原始节点流不是FileInputStream时,采用BufferInputStream不一定能提高性能效率。
FileOutputStream和BufferOutputStream是同样的道理,这里不再阐述。
如果不正确,感谢指正。