BufferedInputStream与BufferedOutputStream的缓存底层实现

  1. 首先观察BufferedInputStream 的继承体系,可以看出他是继承自一个FilterInputStream,而这个又是继承自InputStream

  2. 我们在之前的装饰器模式就讲过,这个BufferedInputStream 是很典型的装饰器模式的案例,那么在其内部应该有封装一个被包装对象

BufferedInputStream 成员变量

class BufferedInputStream extends FilterInputStream {

    private static int DEFAULT_BUFFER_SIZE = 8192;

    private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;

    protected volatile byte buf[];

    private static final
        AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
        AtomicReferenceFieldUpdater.newUpdater
        (BufferedInputStream.class,  byte[].class, "buf");

    protected int count;

    protected int pos;

    protected int markpos = -1;

    protected int marklimit;
  1. 1.但是我们观察其成员变量,我们发现它的内部并没有维护一个被包装对象,这时候我们可以再其父类FilterInputStream 中找到如下代码,在父类中封装了待包装的对象
public class FilterInputStream extends InputStream {

    //此处封装了被包装对象
    protected volatile InputStream in;

    protected FilterInputStream(InputStream in) {
        this.in = in;
    }
  1. 2.在BufferedInputStream 初始化的时候:可以看到调用super(in)父类的构造器,传入一个被包装对象,实现了装饰器模式
    public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }

  1. 接下来对所有的成员变量进行分析
    //根据名字我们可以得知,这个为默认_缓冲区_大小:为8kb
    private static int DEFAULT_BUFFER_SIZE = 8192;

    //此处为最大的缓冲区数组的大小,也就是Integer的最大值,但是数组也要保存数组本身的信息,比如数组长度等信息,也是保存在数组内部,所以减去8个位置
    private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;

    //这里就是我们最重要的缓冲数组,默认每次从文件中读取8kb,如果文件剩余不足8kb,则count等于读入的长度
    protected volatile byte buf[];

    //这里不知道,所以不讨论
    private static final
        AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
        AtomicReferenceFieldUpdater.newUpdater
        (BufferedInputStream.class,  byte[].class, "buf");

    //这里是当前数组中可以读取的容量大小,如果此时buf数组中是满的,则count等于buf.length,如果此时不是满的,则count等于数组中总的可以读取的长度
    protected int count;

    //这个是当前缓冲区中已经读到的位置,例:缓冲区有8192个字节,此时读取了8个字节,则pos等于9,也就是下一个要读取的字节
    protected int pos;

    //标记位置,默认为-1
    protected int markpos = -1;

    protected int marklimit;

  1. 我们接下来对构造函数进行分析:有两个,一个单参,一个双参,我们可以看到单参是会通过this去调用双参的构造器,并且第二个参数是默认的缓冲区大小
    在双参构造其中,super调用父类的构造器,传入被包装流,并且new了一个size长度的数组,如果没有传入缓冲区大小,则使用8192(aka 8kb)来创建缓冲区,否则按照传入的size来创建缓冲区
    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];
    }
  1. 接下来就是BufferedInputStream流中最重要的read方法,首先是read()的无参方法,也就是默认先读取8192字节到缓存中,然后再一个一个从数组中取。

首先是基本使用read()

    public static void main(String[] args) throws Exception {
        String path = "src/main/java/com/xj/dayio/demo.txt";
        FileInputStream fis = new FileInputStream(path);
        BufferedInputStream bis = new BufferedInputStream(fis);

        
        int len = -1;
        while((len = bis.read()) != -1){
            System.out.print((char)len);
        }
    }

然后是read()的源码:可以看出很简短,因为大部分代码都在fill()里面

    public synchronized int read() throws IOException {
        //pos代表当前缓冲数组中已经读到的位置,count是缓冲数组所有可以读的字节数
        //如果当前位置大于或者等于所有可以读的字节数,那么表示缓冲数组中已经没有可以读的字节了,所以需要再次从调用fill填充缓存数组
        if (pos >= count) {
            //填充满后
            fill();
            //如果此时pos位置还是大于或者等于count说明文件中已经没有字节可以读了,于是返回
            if (pos >= count)
                return -1;
        }
        //之后调用getBufIfOpen返回缓冲数组,并且返回当前pos位置的值
        //getBufIfOpen我们看名字就可以得知,如果当前流没有关闭,就获取到缓冲数组
        //此处与0xff具体原因未知,目前以我的理解是将两个字节的byte值提升为四个字节的int值
        return getBufIfOpen()[pos++] & 0xff;
    }

接下来我们具体看填充方法fill()的实现:我们可以看到这个是很长的,8着急,慢慢看

    private void fill() throws IOException {
        //首先拿到缓冲数组
        byte[] buffer = getBufIfOpen();
        //判断标记位置,默认为-1(关于标记先不讲,一会再说)
        if (markpos < 0) //默认为-1,所以将pos当前位置,置为数组的开头
            pos = 0;            
        else if (pos >= buffer.length)  //由于默认值为-1,所以下面这一大段!else if!都不用看啦!暂时。。。
            if (markpos > 0) {  
                int sz = pos - markpos;
                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 {            
                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)) {
                    throw new IOException("Stream closed");
                }
                buffer = nbuf;
            }
        //跳到这里啦!
        //首先让count(也就是可以最多可读字节数量)等于我们的pos(aka 0)
        count = pos;
        //然后我们看这个方法的名字getInIfOpen,就是如果当前流没有被关闭,就获取到真正被包装的输入流对象
        //然后调用真正的输入流对象的read方法(缓冲数组,当前位置(0),缓冲数组的长度(默认8192)-当前位置(0))
        //默认来说的话,就是从文件中读取一个buffer数组长度的字节,并读取到buffer中,那么这里为什么会这么复杂呢,因为是为了标记用的(也就是我们上面跳过的那一大段else if)
        int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
        //如果读取到返回的字节数量大于0的话,就说明读取到了字节,之后让count等于当前位置(pos)加上读取到的字节数量(n)
        //否则n小于0或者等于0的话就说明文件中已经没有字节可读了
        if (n > 0)
            count = n + pos;

        //如果没有字节了的话此时pos和count都为0,然后我们回到我们上一个read的源码,就可以判断出pos >= count,就返回-1,读取结束
    }

  1. 那接下来就是read(byte[] buf)的无参方法,也就是默认先读取8192字节到缓存中,但是与无参的方法区别是:
    • 我们使用无参方法的话是一个一个从缓冲数组中取;
    • 而使用传入缓冲数组的方法,我们每次可以每次从BufferedInputStream 的缓冲数组中每次取一个buf数组长度(4096)的字节;这样子效率更高,不需要一个一个访问bis的缓冲数组
    public static void main(String[] args) throws Exception {
        String path = "src/main/java/com/xj/dayio/demo.txt";
        FileInputStream fis = new FileInputStream(path);
        BufferedInputStream bis = new BufferedInputStream(fis);

        //这里是自定义缓冲数组的大小,(注意:这个不是BufferedInputStream 内部的缓冲数组,是我们自己的)
        //这里仿佛是使用4096时效率更高,并没有具体的考量,只是分别测试了一下
        byte[] buf = new byte[4096];
        int len = -1;
        while((len = bis.read(buf)) != -1){
            String str = new String(buf,0,len);
            System.out.print(str);
        }
    }

啊,先更新到这里,好累呀

posted @ 2020-08-24 22:51  微花  阅读(246)  评论(0编辑  收藏  举报

Loading