IO体系——FileInputStream详解和BufferInputStream真的可以提高效率吗?

前言

作为操作文件IO流最基本的类,应该是每个学习Java的人必知必会的。以前只是会用,不能理解什么是流,不能理解为什么用完必须要close(),更不能理解为什么用了BufferInputStream、BufferOutputStream后速度更快一点,对于这一点,虽然网上很多博客都说这是Buffer包装流带来的功能,但是真相真的是这样吗?
本章将会揭开这一系列问题神秘的面纱,寻找真相。

节点流

在BIO体系中,可以分为节点流和处理流,节点流是指从一个节点开始流向另一个地方的流,在我的理解当中,节点流是一个流对象最初的形态。FileInputStream和FileOutputStream作为操作文件的节点流,它们代表了不同的流向,除此以外,其他无太大区别。
以FileInputStream为例,本章解析它的源码,结合操作系统知识来分析整个过程。

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有什么不同呢?

在研究这两种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就是自带了一个缓存区。如果缓存没有命中,就调用原始节点流的public int read(byte b[], int off, int len)方法将数据加载到BufferInputStream的缓存区中。

但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()的执行过程是怎样的?

由于IO操作是对硬件资源的操作,应该由操作系统来控制,而read0是由JVM来请求。所以整个过程就是CPU处于用户态,JVM进程向操作系统发出请求,CPU切换到内核态,操作系统调度硬盘数据到操作系统的内存中,CPU再度切换到用户态,再由操作系统的内存复制到JVM内存中,最后响应到read0的返回值,返回1个字节数据。不过操作系统一般有预读功能,也就是一次会取出一个单元的数据到内存中,比如4个字节。通过read0读取第2个字节时,就不一定会走到硬盘数据环节,而是在内存中就命中数据了。

如果要请求更多的字节,还得这样走一遍。所以这是很消耗系统资源的操作。

第二种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是同样的道理,这里不再阐述。
如果不正确,感谢指正。

你可能感兴趣的:(IO体系,java)