【原創】從源碼剖析IO流(三)緩存流--轉載請註明出處

一、BufferedInputStream

關於BufferedInputStream,首先我們要看一下,官方給予的對於BufferedInputStream這個類的備註:

/**
 * A <code>BufferedInputStream</code> adds
 * functionality to another input stream-namely,
 * the ability to buffer the input and to
 * support the <code>mark</code> and <code>reset</code>
 * methods. When  the <code>BufferedInputStream</code>
 * is created, an internal buffer array is
 * created. As bytes  from the stream are read
 * or skipped, the internal buffer is refilled
 * as necessary  from the contained input stream,
 * many bytes at a time. The <code>mark</code>
 * operation  remembers a point in the input
 * stream and the <code>reset</code> operation
 * causes all the  bytes read since the most
 * recent <code>mark</code> operation to be
 * reread before new bytes are  taken from
 * the contained input stream.
 *
 * @author  Arthur van Hoff
 * @since   JDK1.0
 */

這段備註的翻譯爲:

對另一個輸入流的功能,即緩衝輸入和支持<代碼>標記< /代碼>和<代碼>重置< /代碼>方法。當創建<代碼> BufferedInputStream</Cuff>時,創建一個內部緩衝區數組。當從流讀取字節或跳過字節時,內部緩衝區根據需要從所包含的輸入流中重新填充,一次多個字節。<代碼>標記< /代碼>操作記住輸入流中的一個點,而<代碼>重置< /代碼>操作會導致自重新開始以來讀取的所有字節。在從包含的輸入流中獲取新字節之前,應重新分配“代碼>代碼>標記<代碼>操作。

要注意上面的標紅的部分,BufferedInputStream和InputStream的主要區別,便是在於BufferedInputStream在一開始使用的時候,會進行初始化一個數組緩衝區,在每次進行操作時,均是從該緩衝區中進行讀取內容。在緩存輸入流中,主要具有以下的成員屬性:

//緩衝區默認的默認大小
    private static int DEFAULT_BUFFER_SIZE = 8192;

    /**
     * 分派給arrays的最大容量
     * 爲什麼要減去8呢?
     * 因爲某些VM會在數組中保留一些頭字,嘗試分配這個最大存儲容量,
     * 可能會導致array容量大於VM的limit,最終導致OutOfMemoryError。
     */
    private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 存放數據的內部緩衝數組。
     * 當有必要時,可能會被另一個不同容量的數組替代。
     */
    protected volatile byte buf[];

    /**
     * 爲緩衝區提供compareAndSet的原子更新器。
     * 這是很有必要的,因爲關閉操作可以使異步的。我們使用非空的緩衝區數組作爲流被關閉的指示器。
     * 該成員變量與buf數組的volatile關鍵字共同作用,實現了當在多線程環境中操作BufferedInputStream對象時,buf和bufUpdater都具有原子性。
     */
    private static final
        AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
        AtomicReferenceFieldUpdater.newUpdater
        (BufferedInputStream.class,  byte[].class, "buf");

    /**
     * 緩衝區中的字節數。
     */
    protected int count;

    /**
     * 緩衝區當前位置的索引
     */
    protected int pos;

    /**
     * 最後一次調用mark方法時pos字段的值。
     */
    protected int markpos = -1;

    /**
     * 調用mark方法後,在後續調用reset方法失敗之前所允許的最大提前讀取量。
     * markpos的最大值
     */
    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];
    }

這裏在進行初始化時,可以使用一個默認的,或者自定義的長度來定義一個緩衝區字節數組的大小。但是在進行構造器構造時,並沒有對緩衝區的內容進行初始化操作。此時,我們就需要來看到BufferedInputStream中的read,read1和fill三個方法:

    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 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. */
            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;
    }
    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:直接讀取所需要的字節,然後組裝進參數b中。如果沒有讀取到足夠的字節,流中依然存在數據可以讀取,則再次調用read1方法進行讀取。

read1:直接讀取所需要的字節,然後組裝進參數b中,如果當前的緩存區數組中的數據已經被全部讀取完畢了,則重新調用fill方法進行讀取新的緩衝區內容。

fill:讀取文件中指定長度的內容到緩衝區中。

由此,可以畫出下方的流程圖:

最後再來看一下Close方法中的操作:

close方法中,會對所持有的InputStream對象進行關閉,然後對內部存在的緩衝區的對象進行清除。

    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總結:

緩存輸入流,本身是以一個byte類型的數組作爲緩存容器,優先將輸入流中的內容讀取到緩存容器中,然後再從容器中輸出到外部來供使用者使用的類。利用的是輸入流在一次讀取較多字節時,效率高於多次讀取較少字節來的特點來設計的。以此降低從輸入流中讀取字節的頻率,以達到對於長內容,在較短讀取字節長度下快速讀取的功能。對於該功能我們寫出以下的用例來進行測試:

    public static void main(String args[]) throws Exception{
        {
            long startTime = System.currentTimeMillis();
            for (int i = 1; i <= 100; i++) {
                FileInputStream fis = new FileInputStream("E:/testFile/test.txt");
                for (byte[] b = new byte[1000]; fis.read(b, 0, 1000) > 0; ) {
                }
                fis.close();
            }
            long endTime = System.currentTimeMillis();
            System.out.println("每次讀取1000字節用時:" + (endTime - startTime));
        }
        {
            long startTime = System.currentTimeMillis();
            for (int i = 1 ; i <= 100 ; i ++) {
                FileInputStream fis = new FileInputStream("E:/testFile/test.txt");
                for (byte[] b = new byte[1000] ; fis.read(b, 0, 1) > 0 ; ) {}
                fis.close();
            }
            long endTime = System.currentTimeMillis();
            System.out.println("每次讀取1字節用時:" + (endTime - startTime));
        }
    }

執行的結果爲:

每次讀取1000字節用時:4
每次讀取1字節用時:50

三、BufferedOutputStream

在學習完了BufferedInputStream之後,再來看BufferedOutPutStream就能很好的理解這個流的內容了。

首先我們需要看一下BufferedInputStream中的flushBuffer方法,該方法是將已經緩存的所有的字節,調用輸出流,將字節刷新輸出。這個方法會在close()方法和write方法中進行使用。

    private void flushBuffer() throws IOException {
        if (count > 0) {
            out.write(buf, 0, count);
            count = 0;
        }
    }

接下來我們只要來看一下write方法即可:

    public synchronized void write(byte b[], int off, int len) throws IOException {
        if (len >= buf.length) {
            /* If the request length exceeds the size of the output buffer,
               flush the output buffer and then write the data directly.
               In this way buffered streams will cascade harmlessly. */
            flushBuffer();
            out.write(b, off, len);
            return;
        }
        if (len > buf.length - count) {
            flushBuffer();
        }
        System.arraycopy(b, off, buf, count, len);
        count += len;
    }

wirte方法和BufferedInputStream中的read方法相反,read方法爲先寫入到緩存區數組中,再輸出給用戶,而write方法爲先緩存到緩存區,當緩存區存儲滿了以後再輸出到進行輸出到文件或其他地方。

在BufferedOutPutStream中,沒有提供出特殊的Close()方法,而是使用了其父類FilterOutPutStream中的Close方法,關閉自己所持有的out對象後,再調用flush方法進行刷新。即,在Close方法調用時,將會直接關閉成員屬性out,然後將所有的緩存內容寫入到輸出流中。

    public synchronized void flush() throws IOException {
        flushBuffer();
        out.flush();
    }

四、BufferedOutPutStream總結:

緩存輸入流是對流中的內容進行批量讀取,然後進行分段輸出給使用者,而緩存輸出流,則是將分段輸出的內容,緩存到緩存區數組中,然後進行批量寫入到輸出流中。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章