Java8之BufferedInputStream源码

13 篇文章 3 订阅

参考文章:

《java.io.BufferedInputStream 源码分析》

《IO源码解析--一文说尽BufferedInputStream》

《BufferedInputStream 源码学习笔记》

《JavaIO之BufferedInputStream详解》

写在开头:本文为学习后的总结,可能有不到位的地方,错误的地方,欢迎各位指正。

前段时间遇到个需求涉及到了IO,趁这机会重新复习了下,这里简单记录下对缓冲流的理解。

         Java中的缓冲流BufferedInputStream继承自字节流,通过减少从磁盘读取文件的次数实现了文件读写的提速。可以简单的理解为,直接使用FileInputStream每次从磁盘读取1KB数据进入内存,而缓冲流直接每次读取8KB,通过这样的方式减少了磁盘的读取次数。

Object(java.lang)
  -- InputStream(java.io)
    -- FilterInputStream(java.io)
      -- BufferedInputStream(java.io)

目录

一、基本功能介绍

二、主体方法

        1、流的操作

        2、辅助功能

三、读取方法

        1、public int read(byte b[])

        2、public synchronized int read()

        3、private int read1(byte[] b, int off, int len)

        4、public synchronized int read(byte b[], int off, int len)


一、基本功能介绍

// 默认的缓冲池大小 8KB
private static int DEFAULT_BUFFER_SIZE = 8192;

// 最大的缓冲池大小(-8的原因是兼容某些虚拟机数组自带的头信息)
private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;

// 缓冲数组
protected volatile byte buf[];

// 原子更新器,保证了数组的原子性,防止在 buffer 被关闭的情况下修改 buffer 数组.判断 buffer 是否被关闭的条件是 buf 数组是否为 null 
private static final
        AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
        AtomicReferenceFieldUpdater.newUpdater
        (BufferedInputStream.class,  byte[].class, "buf");

		
// 当前缓冲区末尾的位置
protected int count;

// 当前缓冲区中读取位置的索引
protected int pos;


// 是否开启重复读的标记位
// markpos和reset()配合使用才有意义.操作步骤:
// 1.通过mark() 函数,保存pos的值到markpos中。
// 2.通过reset() 函数,会将pos的值重置为markpos。
protected int markpos = -1;

// 可重复读的最大长度,即markpos到pos的长度限制
protected int marklimit;
    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];
    }

 count、pos、markpos的位置关系如下。

         BufferedInputStream在内部构建一个缓冲池,每次会从数据源获取一定容量的数据填充到缓冲池中,在这个缓冲池中,pos是当前读取位置的索引,count是缓冲池末尾的位置。markpos默认值-1,可以通过mark(marklimit)方法将值设置为大于等于0,此时我们可以调用reset方法对已读取过的文件进行重复读取(即从markpos到pos这段的数据)。但这个重复读的长度限制也是有限制的,即marklimit。

        

二、主体方法

        1、流的操作

        1.1、getInIfOpen():获取底层 InputStream 的对象,如果流被关闭就抛异常.该方法除了获取底层流还可以判断流是否关闭,通过抛异常终端后续操作.

        1.2、getBufIfOpen():获取 buffer 数组对象引用,如果流被关闭就抛异常.所以该方法除了获取底层流还可以判断流是否关闭,通过抛异常终端后续操作.

        1.3、close(): 操作很简单,就是置空缓存数组和关闭流.

// 获取输入流
private InputStream getInIfOpen() throws IOException {
    InputStream input = in;
    if (input == null)
        throw new IOException("Stream closed");
    return input;
}

// 获取缓冲区
private byte[] getBufIfOpen() throws IOException {
    byte[] buffer = buf;
    if (buffer == null)
        throw new IOException("Stream closed");
    return buffer;
}


// 关闭输入流
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()
    }
}

        2、辅助功能

        1. available():返回还有多少字节可读。n代表当前缓冲区的可读字节数,avail代表数据源的可读字节数。返回 (n + avail) 与 MAX_VALUE 中较小的一方。

// 返回还有多少字节可读
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;
}

        2. skip():跳过一定数量字节

        计算当前缓冲区的未写出字节数为avail:

        (1)如果avail大于0,跳过n与avail中较小的数量的字节,返回这个较小的数。

        (2)如果avail小于等于0:

                (2.1)无标记,直接在源输入流跳过n个字节。
                (2.2)有标记,将被标记的那段缓存往左移动首处,然后重新计算avail,如果仍然小于等于0,则返回0,代表跳过了0个字节

// 跳过一定数量字节
public synchronized long skip(long n) throws IOException {
    getBufIfOpen(); // Check for closed stream
    if (n <= 0) {
        return 0;
    }
    long avail = count - pos;

    if (avail <= 0) {
        // If no mark position set then don't keep in buffer
        if (markpos <0)
            return getInIfOpen().skip(n);

        // Fill in buffer to save bytes for reset
        fill();
        avail = count - pos;
        if (avail <= 0)
            return 0;
    }

    long skipped = (avail < n) ? avail : n;
    pos += skipped;
    return skipped;
}

        3. mark():开启重复读功能,参数readlimit用来限制重复读的最大长度,同时将当前读取位置的索引pos赋值给markpos,供reset使用

// 开启重复读功能
public synchronized void mark(int readlimit) {
    marklimit = readlimit;
    markpos = pos;
}

        4. reset():重置读取位置,将读取位置pos重新定位到之前调用mark方法时标记的重复读的起始位置。

// 重置函数
public synchronized void reset() throws IOException {
    getBufIfOpen(); // Cause exception if closed
    if (markpos < 0)
        throw new IOException("Resetting to invalid mark");
    pos = markpos;
}

        5. fill():填充缓冲区。fill方法是缓冲区填充的重要逻辑,这里我再拆分成几块来简化理解。

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;
}
private void fill() throws IOException {
    byte[] buffer = getBufIfOpen();
    if (markpos < 0){
        // (1) 未启用重复读 markpos = -1
    }else if (pos >= buffer.length){
        // (2) 启用重复读 markpos >=0
        if (markpos > 0){  // (2.1)markpos > 0          
        }else if (buffer.length >= marklimit){  // (2.2)markpos == 0           
        }else if (buffer.length >= MAX_BUFFER_SIZE) {  //  (2.3)溢出判断            
        }else{    // (2.4)正常扩容            
        }
    }
    count = pos;
    // 从数据源获取数据
    int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
    if (n > 0)
        // 读取数据后计算出新的 count
        count = n + pos;
}

        (1)markpos < 0:未开启重复读功能,直接将pos设置为0,从头读取

        (2)markpos >=0且pos>=buffer.length:

                (2.1)markpos > 0 :

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;
 } 

        因为markpos>0,所以必然有pos - markpos<buffer.length,需要把markpos-pos这一段

复制到buffer的开头进行保存,同时将markpos置位0。

                (2.2)buffer.length >= marklimit:

else if (buffer.length >= marklimit) {
     markpos = -1;   /* buffer got too big, invalidate mark */
     pos = 0;        /* drop buffer contents */
}

        这一步根据上面的判断筛选后必然存在markpos==0,相当于需要重复读的长度为整个buffer数组,这里就需要确认buffer.length是否超过了允许重复读的长度上限marklimit。因此直接将markpos置为-1,关闭重复读,同时将pos置为0,buffer从头开始写。

                (2.3)buffer.length >= MAX_BUFFER_SIZE:数组长度大于规定容量最大值 MAX_BUFFER_SIZE,内存溢出。

else if (buffer.length >= MAX_BUFFER_SIZE) {
    throw new OutOfMemoryError("Required array size too large");
} 

              (2.4)正常扩容:

else {
    // pos <= MAX_BUFFER_SIZE - pos 相当于判断 pos 大小是否大于 MAX_BUFFER_SIZE 的一半
    // nsz 为 buffer 容量扩充后的大小
    //      pos >=  MAX_BUFFER_SIZE/2 时 nsz = MAX_BUFFER_SIZE
    //      pos < MAX_BUFFER_SIZE/2   时 nsz = pos * 2
    int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
      pos * 2 : MAX_BUFFER_SIZE;
    if (nsz > marklimit)
      // nsz 大小不可以超过 marklimit
      nsz = marklimit;
    // 创建大小为 nsz 新的 buffer 数组
    byte nbuf[] = new byte[nsz];
    // 把旧数组中的数据复制到新的数组中
    System.arraycopy(buffer, 0, nbuf, 0, pos);
    if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
      // 在高并发场景下,调用 compareAndSet() 可保证流还没有被关闭
      throw new IOException("Stream closed");
    }
    // 把 buffer 底层字节数组换成新的扩容后的数组
    buffer = nbuf;
  }

        取2倍pos,marklimit以及MAX_BUFFER_SIZE三者中最小的数为size,创建一个byte数组,并将当前缓冲区的所有数据拷贝到这个扩大后的数组中,另这个扩大后的数组为当前类的缓冲区。 

   

三、读取方法

        1、public int read(byte b[])

// FilterInputStream.java
public int read(byte b[]) throws IOException {
  return read(b, 0, b.length);
}

        2、public synchronized int read()

/**
 * 返回下一个字节
 */
public synchronized int read() throws IOException {
    if (pos >= count) {
        // pos >= count 表示当前 buffer 内的所有数据都读完了
        // 调用 fill() 往 buffer 内添加新的数据
        fill();
        // 如果 pos >= count 依然成立,返回 -1 表示数据已读完
        if (pos >= count)
            return -1;
    }
    // 返回 buffer 有效数据的第一个字节
    return getBufIfOpen()[pos++] & 0xff;
}

         注意:最后返回之前还要做一次位运算,原因是 buf[] 是 byte 数组,read() 返回的是 int ,如何把一个 byte 变成一个 int 呢,答案就是补零,把 8 位的 byte 变成 32位的 int.

        3、private int read1(byte[] b, int off, int len)

/**
 * @param b 读取数据存放的目标数组
 * @param off 偏移值,表示 b 数组可以写的第一个字节的下标
 * @param len 读取的数据大小
 */
private int read1(byte[] b, int off, int len) throws IOException {
    // avail 表示缓存数组 buf[] 可写空间大小
    int avail = count - pos;
    // avail <= 0 表示缓存数组已写满
    if (avail <= 0) {
        /* If the requested length is at least as large as the buffer, and
           if there is no mark/reset activity, do not bother to copy the
             bytes into the local buffer.  In this way buffered streams will
           cascade harmlessly. */
        /* 如果读取的大小 >= 缓存 buffer 数组长度,且 mark 机制是无效的时候,
           不用纠结还想着先把数据复制到缓存中,直接从 InputStream 中读取数据即可.*/
        if (len >= getBufIfOpen().length && markpos < 0) {
            // 内部调用了 read()
            return getInIfOpen().read(b, off, len);
        }
        // 往缓存中添加数据
        fill();
        // 如果当前
        avail = count - pos;
        if (avail <= 0) return -1;
    }
    // cnt 表示需要复制的数据长度
    int cnt = (avail < len) ? avail : len;
    // 从 buffer 中获取数据范围 [pos, pos+cnt] 数据到 b 中范围 [off, off+cnt] 中
    System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
    // 更新缓存中下一个可读字节下标 pos
    pos += cnt;
    return cnt;
}

        4、public synchronized int read(byte b[], int off, int len)

// BufferedInputStream.java
/**
 * @param b 读取数据存放的目标数组
 * @param off 偏移值,表示 b 数组可以写的第一个字节的下标
 * @param len 读取的数据大小
 */
public synchronized int read(byte b[], int off, int len)
    throws IOException
{
    // 检查流是否被关闭
    getBufIfOpen();
    // 对 read() 传入参数做校验,防止数组边界越界
    if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
        throw new IndexOutOfBoundsException();
    } else if (len == 0) {
        return 0;
    }

    // n 记录已读数据大小
    int n = 0;
    for (;;) {
        // 调用 read1 方法往数组 b 中添加数据, nread 为添加的数据大小
        int nread = read1(b, off + n, len - n);
        if (nread <= 0)
            return (n == 0) ? nread : n;
        n += nread;
        // 满足 nread > 0 && n >= len 
        // 判断数据是否已经读完了
        if (n >= len)
            return n;
        InputStream input = in;
        // 满足 nread > 0 && n < len 
        // 判断 inputstream 中是否还有未读数据
        if (input != null && input.available() <= 0)
            return n;
    }
}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值