1 概述

缓冲流,也叫高效流,是对字节流FileOutputStreamFileInputStream),字符流FileReaderFileWriter)的增强。

缓冲流按照数据类型分类:

  • 字节缓冲流:BufferedOutputStreamBufferedInputStream
  • 字符缓冲流:BufferedWriterBufferedReader

缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统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的源码可以得出:

  1. 默认的构造方法会在内部 新建一个容量为8192的byte[]数组。
  2. 每次写入字节时,只会将该字节存入内部缓冲区中。
  3. 只有当缓冲区中的字节个数超过最大容量,才会调用底层操作系统,将缓冲区中的全部字节写入。

3 BufferedInputStream【字节缓冲输入流】

java.io.BufferedInputStream为字节流添加新功能,即缓冲输入、支持markreset方法。

在创建 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. 补码一致,将有符号数转为无符号数
         * 推荐博客:
         */
        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的源码可以得知:

  1. 尽管read()方法只读取一个字节,也会一次性尝试从底层阅读足以填充整个缓冲区的字节数量。
  2. 在一些特定的情况,内部缓冲区会进行扩容,正常情况下会扩容至两倍当前位置的容量。
  3. 内部缓冲区被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毫秒