javaI/O之BufferedInputStream详解编程语言

javaI/O之BufferedInputStream

 

javaI/O之BufferedInputStream

一、介绍

    BufferedInputStream类继承了FilterInputStream对象,是inputStream的修饰者类。能够将输入流先进行缓存,避免每次都需要进行实质性的I/O操作。第一次会缓存8192字节的数据,然后会成倍增长。

二、属性

    privatestatic int defaultBufferSize = 8192;默认缓存区大小。

    protectedvolatile byte buf[];缓冲区数组。

三、构造函数

    publicBufferedInputStream(InputStream in) ;in是被修饰的输入流,创建默认大小的缓冲区。

    publicBufferedInputStream(InputStream in, int size);in是被修饰的输入流,创建指定大小的缓冲区。

四、方法

    publicsynchronized int read() throws IOException;读取缓冲区中的一个字节,缓冲区被全部读取的话讲缓冲区加倍然后填入数据。到达输入流的最后则返回-1;

    publicsynchronized int read(byte b[], int off, int len);读取缓冲区中len个字节,缓冲区被全部读取的话讲缓冲区加倍然后填入数据然后继续读取。返回实际读取的字节数,或则-1;

    publicsynchronized long skip(long n) throws IOException;跳过N个字节数据,直到流的最后,返回实际跳过的字节数或则-1;

    publicsynchronized int available() throws IOException;返回流的剩余字节数。

    publicsynchronized void mark(int readlimit);标记流读取的当前位置。

    publicsynchronized void reset() throws IOException;返回到最近一次mark的位置。

public void close() throws IOException关闭当前数据流;

 

 

 

 

 

BufferedInputStream是一个带有缓冲区域的InputStream,它的继承体系如下: 

InputStream 
|__FilterInputStream 
        |__BufferedInputStream
 

首先了解一下FilterInputStream: 
FilterInputStream通过装饰器模式将InputStream封装至内部的一个成员变量: 

Java代码  

1.  protected volatile InputStream in;  

需要注意的是该成员变量使用了volatile关键字进行修饰,这意味着该成员变量的引用的内存可见性为多线程即时可见的。 
其它地方FilterInputStream将所有的操作委托给了in这个成员进行操作。 

 

了解了这些过后,来仔细看看BufferedInputStream的成员变量: 

Java代码  

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

2.    

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

4.    

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

6.    

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

8.    

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

10.   

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

12.   

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

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

Java代码  

1.  public BufferedInputStream(InputStream in, int size) {  

2.      super(in);  

3.          if (size <= 0) {  

4.              throw new IllegalArgumentException(“Buffer size <= 0”);  

5.          }  

6.      buf = new byte[size];  

7.  }  

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

Java代码  

1.  private void fill() throws IOException {  

2.          byte[] buffer = getBufIfOpen();  

3.      if (markpos < 0) {  

4.            /*如果不存在标记位置(即没有需要进行reset的位置需求) 

5.              则可以进行大胆地直接重置pos标识下一可读取位置,但是这样 

6.              不是会读取到以前的旧数据吗?不用担心,在后面的代码里会实现输入流的新  

7.              数据填充*/  

8.          pos = 0;          

9.      }else if (pos >= buffer.length){  

10.        /* 位置大于缓冲区长度,这里表示已经没有可用空间了 */  

11.         if (markpos > 0) {     

12.              /* 表示存在mark位置,则要对mark位置到pos位置的数据予以保留, 

13.                 以确保后面如果调用reset()重新从mark位置读取会取得成功*/  

14.         int sz = pos – markpos;  

15.                 /*该实现是通过将缓冲区域中markpospos部分的移至缓冲区头部实现*/  

16.         System.arraycopy(buffer, markpos, buffer, 0, sz);  

17.         pos = sz;  

18.         markpos = 0;  

19.         } else if (buffer.length >= marklimit) {  

20.                 /* 如果缓冲区已经足够大,可以容纳marklimit,则直接重置*/  

21.                 markpos = –1;     

22.         pos = 0;/* 丢弃所有的缓冲区内容 */  

23.         } else {          

24.                 /* 如果缓冲区还能增长的空间,则进行缓冲区扩容*/  

25.         int nsz = pos * 2;  

26.                 /*新的缓冲区大小设置成满足最大标记极限即可*/  

27.         if (nsz > marklimit)  

28.             nsz = marklimit;  

29.         byte nbuf[] = new byte[nsz];  

30.                 //将原来的较小的缓冲内容COPY至增容的新缓冲区中  

31.         System.arraycopy(buffer, 0, nbuf, 0, pos);  

32.                 //这里使用了原子变量引用更新,确保多线程环境下内存的可见性  

33.                 if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {  

34.                     // Can’t replace buf if there was an async close.  

35.                     // Note: This would need to be changed if fill()  

36.                     // is ever made accessible to multiple threads.  

37.                     // But for now, the only way CAS can fail is via close.  

38.                     // assert buf == null;  

39.                     throw new IOException(“Stream closed”);  

40.                 }  

41.                 buffer = nbuf;  

42.         }  

43.         count = pos;  

44.         //从原始输入流中读取数据,填充缓冲区  

45.     int n = getInIfOpen().read(buffer, pos, buffer.length – pos);  

46.         //根据实际读取的字节数更新缓冲区中可用字节数  

47.         if (n > 0)  

48.             count = n + pos;  

49.     }  

整个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表示). 

Java代码  

1.  public synchronized int read() throws IOException {  

2.      if (pos >= count) {  

3.             /*表示读取位置已经超过了缓冲区可用范围,则对缓冲区进行重新填充*/  

4.          fill();  

5.             /*当填充后再次读取时发现没有数据可读,证明读到了流末尾*/  

6.          if (pos >= count)  

7.          return –1;  

8.      }  

9.          /*这里表示读取位置尚未超过缓冲区有效范围,直接返回缓冲区内容*/  

10.     return getBufIfOpen()[pos++] & 0xff;  

11. }  

一次读取多个字节(尽量读,非贪婪) 

Java代码  

1.  private int read1(byte[] b, int off, int len) throws IOException {  

2.      int avail = count – pos;  

3.      if (avail <= 0) {  

4.          /*这里使用了一个巧妙的机制,如果读取的长度大于缓冲区的长度 

5.                并且没有markpos,则直接从原始输入流中进行读取,从而避免无谓的 

6.                COPY(从原始输入流至缓冲区,读取缓冲区全部数据,清空缓冲区, 

7.                重新填入原始输入流数据)*/  

8.          if (len >= getBufIfOpen().length && markpos < 0) {  

9.          return getInIfOpen().read(b, off, len);  

10.         }  

11.             /*当无数据可读时,从原始流中载入数据到缓冲区中*/  

12.         fill();  

13.         avail = count – pos;  

14.         if (avail <= 0return –1;  

15.     }  

16.     int cnt = (avail < len) ? avail : len;  

17.         /*从缓冲区中读取数据,返回实际读取到的大小*/  

18.     System.arraycopy(getBufIfOpen(), pos, b, off, cnt);  

19.     pos += cnt;  

20.     return cnt;  

21.     }  

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

Java代码  

1.  public synchronized int read(byte b[], int off, int len)  

2.      throws IOException  

3.      {  

4.          getBufIfOpen(); // Check for closed stream  

5.          if ((off | len | (off + len) | (b.length – (off + len))) < 0) {  

6.          throw new IndexOutOfBoundsException();  

7.      } else if (len == 0) {  

8.              return 0;  

9.          }  

10.   

11.     int n = 0;  

12.         for (;;) {  

13.             int nread = read1(b, off + n, len – n);  

14.             if (nread <= 0)   

15.                 return (n == 0) ? nread : n;  

16.             n += nread;  

17.             if (n >= len)  

18.                 return n;  

19.             // if not closed but no bytes available, return  

20.             InputStream input = in;  

21.             if (input != null && input.available() <= 0)  

22.                 return n;  

23.         }  

24.     }  

略过多少字节 

Java代码  

1.  public synchronized long skip(long n) throws IOException {  

2.          getBufIfOpen(); // Check for closed stream  

3.      if (n <= 0) {  

4.          return 0;  

5.      }  

6.      long avail = count – pos;  

7.         

8.          if (avail <= 0) {  

9.              // If no mark position set then don’t keep in buffer  

10.             //从上面的注释可以知道,这也是一个巧妙的方法,如果没有mark标记,  

11.             // 则直接从原始输入流中skip  

12.             if (markpos <0)   

13.                 return getInIfOpen().skip(n);  

14.               

15.             // Fill in buffer to save bytes for reset  

16.             fill();  

17.             avail = count – pos;  

18.             if (avail <= 0)  

19.                 return 0;  

20.         }  

21.         //该方法的实现为尽量原则,不保证一定略过规定的字节数。  

22.         long skipped = (avail < n) ? avail : n;  

23.         pos += skipped;  

24.         return skipped;  

25.     }  

估计目前可用的字节数,原始流中可用的字节数+缓冲区中可用的字节数 

Java代码  

1.  public synchronized int available() throws IOException {  

2.      return getInIfOpen().available() + (count – pos);  

3.      }  

标记位置: 

Java代码  

1.  public synchronized void mark(int readlimit) {  

2.      marklimit = readlimit;  

3.      markpos = pos;  

4.      }  

重置位置:该实现清晰的表明下一读取位置被推到了以前的标记位置,以实现重新读取区段的功能 

Java代码  

1.  public synchronized void reset() throws IOException {  

2.          getBufIfOpen(); // Cause exception if closed  

3.      if (markpos < 0)  

4.          throw new IOException(“Resetting to invalid mark”);  

5.      pos = markpos;  

6.      }  

关闭流:首先通过线程安全的方式设置了内部的缓冲区引用为空,然后再对原始输入流进行关闭。 

Java代码  

1.  public void close() throws IOException {  

2.          byte[] buffer;  

3.          while ( (buffer = buf) != null) {  

4.              if (bufUpdater.compareAndSet(this, buffer, null)) {  

5.                  InputStream input = in;  

6.                  in = null;  

7.                  if (input != null)  

8.                      input.close();  

9.                  return;  

10.             }  

11.             // Else retry in case a new buf was CASed in fill()  

12.         }  

13.     }  

 

    

 

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/13803.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论