JDK1.0-缓冲流

  IO流里面经常用到的就是装饰器模式,也就是常说的封装。装饰器模式就是在原有的基础上增加功能。

  * java.io.BufferedInputStream与java.io.BufferedOutputStream可以为InputStream,OutputStream类增加缓冲区功能。构建BufferedInputStream实例时,需要给定一个InputStream类型的实例,实现BufferedInputStream时,实际上最后是实现InputStream实例。同样,构建BufferedOutputStream时,也需要给定一个OutputStream实例,实现BufferedOutputStream时,实际上最后是实现OutputStream实例。

  * BufferedInputStream继承与FilterInputStream,FilterInputStream通过装饰器模式将InputStream封装至内部的一个成员变量,BufferedInputStream的数据成员buf是一个位数组,默认为2048字节。当读取数据来源时,例如文件,BufferedInputStream会尽量将buf填满(读的比要求读的要多的多)当使用read()方法时,实际上是先读取buf中的数据,而不是直接对数据来源作读取。当buf中的数据不足时,BufferedInputStream才会再实现给定的InputStream对象的read()方法,从指定的装置中提取数据

  * BufferedOutputStream的数据成员buf也是一个位数组,默认为512字节。当使用write()方法写入数据时实际上会先将数据写到buf中,当buf已满时才会实现给定的OutputStream对象的write()方法,将buf数据写到目的地,而不是每次都对目的地作写入的动作为了确保缓冲区中的数据一定被写出至目的地,建议最后执行flush()将缓冲区中的数据全部写出目的流中。

  它们的设计都是为了减少了磁盘IO

  BufferedInputStream源码分析

  BufferedInputStream的成员变量: 

  

private static int defaultBufferSize = 8192 //该变量定义了默认的缓冲大小

protected volatile byte buf[]; //缓冲数组,注意该成员变量同样使用了volatile关键字进行修饰,作用为在多线程环境中,当对该变量引用进行修改时保证了内存的可见性。

private static final AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater = AtomicReferenceFieldUpdater.newUpdater(BufferedInputStream.class,  byte[].class, "buf")//缓存数组的原子更新器,该成员变量与buf数组的volatile关键字共同组成了buf数组的原子更新功能实现。

protected int count;//该成员变量表示目前缓冲区域中有多少有效的字节。

protected int pos;//该成员变量表示了当前缓冲区的读取位置。

protected int markpos = -1;/*表示标记位置,该标记位置的作用为:实现流的标记特性,即流的某个位置可以被设置为标记,允许通过设置reset(),将流的读取位置进行重置到该标记位置,但是InputStream注释上明确表示,该流不会无限的保证标记长度可以无限延长,即markpos=15,pos=139734,该保留区间可能已经超过了保留的极限(如下)*/

protected int marklimit;/*该成员变量表示了上面提到的标记最大保留区间大小,当pos-markpos> marklimit时,mark标记可能会被清除(根据实现确定)。*/

通过构造函数可以看到:初始化了一个byte数组作为缓冲区域 

public BufferedInputStream(InputStream in, int size) {
    super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
    buf = new byte[size];
}

这个类中最为重要的方法是fill()方法,它提供了缓冲区域的读取、写入、区域元素的移动更新等。下面着重分析一下该方法: 

private void fill() throws IOException {
        byte[] buffer = getBufIfOpen();
    if (markpos < 0) {
          /*如果不存在标记位置(即没有需要进行reset的位置需求)
            则可以进行大胆地直接重置pos标识下一可读取位置,但是这样
            不是会读取到以前的旧数据吗?不用担心,在后面的代码里☆会实现输入流的新 
            数据填充*/
        pos = 0;        
    }else if (pos >= buffer.length){
       /* 位置大于缓冲区长度,这里表示已经没有可用空间了 */
        if (markpos > 0) {    
             /* 表示存在mark位置,则要对mark位置到pos位置的数据予以保留,
                以确保后面如果调用reset()重新从mark位置读取会取得成功*/
        int sz = pos - markpos;
                /*该实现是通过将缓冲区域中markpos至pos部分的移至缓冲区头部实现*/
        System.arraycopy(buffer, markpos, buffer, 0, sz);
        pos = sz;
        markpos = 0;
        } else if (buffer.length >= marklimit) {
                /* 如果缓冲区已经足够大,可以容纳marklimit,则直接重置*/
                markpos = -1;    
        pos = 0;/* 丢弃所有的缓冲区内容 */
        } else {        
                /* 如果缓冲区还能增长的空间,则进行缓冲区扩容*/
        int nsz = pos * 2;
                /*新的缓冲区大小设置成满足最大标记极限即可*/
        if (nsz > marklimit)
            nsz = marklimit;
        byte nbuf[] = new byte[nsz];
                //将原来的较小的缓冲内容COPY至增容的新缓冲区中
        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;
    }

  整个fill的过程,可以看作是BufferedInputStream对外提供滑动读取的功能实现,通过预先读入一整段原始输入流数据至缓冲区中,而外界对BufferedInputStream的读取操作实际上是在缓冲区上进行,如果读取的数据超过了缓冲区的范围,那么BufferedInputStream负责重新从原始输入流中载入下一截数据填充缓冲区,然后外界继续通过缓冲区进行数据读取。

  这样的设计的好处是:避免了大量的磁盘IO,因为原始的InputStream类实现的read是即时读取的,即每一次读取都会是一次磁盘IO操作(哪怕只读取了1个字节的数据),可想而知,如果数据量巨大,这样的磁盘消耗非常可怕。而通过缓冲区的实现,读取可以读取缓冲区中的内容,当读取超过缓冲区的内容后再进行一次磁盘IO,载入一段数据填充缓冲,那么下一次读取一般情况下就直接可以从缓冲区读取,减少了磁盘IO。减少的磁盘IO大致可以通过以下方式计算(限read()方式): 

length  流的最终大小 
bufSize 缓冲区大小 

则通过缓冲区实现的输入流BufferedInputStream的磁盘IO数为原始InputStream磁盘IO的 
1/(length/bufSize) 

//read方法解析:该方法返回当前位置的后一位置byte值(int表示)
public
synchronized int read() throws IOException { if (pos >= count) { /*表示读取位置已经超过了缓冲区可用范围,则对缓冲区进行重新填充*/ fill(); /*当填充后再次读取时发现没有数据可读,证明读到了流末尾*/ if (pos >= count) return -1; } /*这里表示读取位置尚未超过缓冲区有效范围,直接返回缓冲区内容*/ return getBufIfOpen()[pos++] & 0xff; } 一次读取多个字节(尽量读private int read1(byte[] b, int off, int len) throws IOException { int avail = count - pos; if (avail <= 0) { /*这里使用了一个巧妙的机制,如果读取的长度大于缓冲区的长度 并且没有markpos,则直接从原始输入流中进行读取,从而避免无谓的 COPY(从原始输入流至缓冲区,读取缓冲区全部数据,清空缓冲区, 重新填入原始输入流数据)*/ 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; }

  
以下方法和上面的方法类似,唯一不同的是,上面的方法是尽量读,读到多少是多少,而下面的方法是贪婪的读,没有读到足够多的数据(len)就不会返回,除非读到了流的末尾。该方法通过不断循环地调用上面read1方法实现贪婪读取。 

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;
        }
    }
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
            //从上面的注释可以知道,这也是一个巧妙的方法,如果没有mark标记,
            // 则直接从原始输入流中skip
            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;
    }
//估计目前可用的字节数,原始流中可用的字节数+缓冲区中可用的字节数 
public synchronized int available() throws IOException {
    return getInIfOpen().available() + (count - pos);
    }
public synchronized void mark(int readlimit) {//标记位置: 
    marklimit = readlimit;
    markpos = pos;
    }
//重置位置:该实现清晰的表明下一读取位置被推到了以前的标记位置,以实现重新读取区段的功能 
public synchronized void reset() throws IOException {
        getBufIfOpen(); // Cause exception if closed
    if (markpos < 0)
        throw new IOException("Resetting to invalid mark");
    pos = markpos;
    }
//关闭流:首先通过线程安全的方式设置了内部的缓冲区引用为空,然后再对原始输入流进行关闭。 
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()
        }
    }

参考:http://blog.sina.com.cn/s/blog_735065f90100pv3g.html

http://icanfly.iteye.com/blog/1207397

原文地址:https://www.cnblogs.com/jslee/p/3443248.html