测试案例

  1. import java.io.*;
  2. /**
  3. * 分别用普通数据流和带缓冲区的数据流复制一个167M的数据文件
  4. * 通过用时比较两者的工作效率
  5. * @author qingfeng
  6. *
  7. */
  8. public class Test {
  9. private static File file = new File("D:\\home\\test.zip");
  10. private static File file_cp = new File("D:\\home\\test.zip");
  11. // FileInputStream复制
  12. public void copy() throws IOException {
  13. FileInputStream in = new FileInputStream(file);
  14. FileOutputStream out = new FileOutputStream(file_cp);
  15. byte[] buf = new byte[1024];
  16. int len = 0;
  17. while ((len = in.read(buf)) != -1) {
  18. out.write(buf);
  19. }
  20. in.close();
  21. out.close();
  22. }
  23. // BufferedStream复制
  24. public void copyByBuffer() throws IOException {
  25. BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));
  26. BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file_cp));
  27. byte[] buf = new byte[1024];
  28. int len;
  29. while ((len = in.read(buf)) != -1) {
  30. out.write(buf);
  31. }
  32. in.close();
  33. out.close();
  34. }
  35. public static void main(String[] args) throws IOException {
  36. Test copy=new Test();
  37. long time1=System.currentTimeMillis();
  38. copy.copy();
  39. long time2=System.currentTimeMillis();
  40. System.out.println("直接复制用时:"+(time2-time1)+"毫秒");
  41. long time3=System.currentTimeMillis();
  42. copy.copyByBuffer();
  43. long time4=System.currentTimeMillis();
  44. System.out.println("缓冲区复制用时:"+(time4-time3)+"毫秒");
  45. }
  46. }

执行结果

image.png

原因分析

上述InputStream使用了byte[] buf = new byte[1024]; 作为read方法的缓冲区。根据运行结果可以知道InputStream的缓冲区与BufferedInputStream的缓冲区是不一样的。那么现在通过查看源码来分析两者有什么不一样。

FileInputStream

  1. package java.io;
  2. public class FileInputStream extends InputStream{
  3. /**
  4. *从输入流中读取一个字节
  5. *该方法为private私有方法,用户不能直接调用。
  6. *该方法为native本地方法,这是因为Java语言不能直接与操作系统或计算机硬件交互,
  7. *只能通过调用C/C++编写的本地方法来实现对磁盘数据的访问。
  8. */
  9. private native int read0() throws IOException;
  10. public int read() throws IOException {
  11. Object traceContext = IoTrace.fileReadBegin(path); //指向Path
  12. int b = 0;
  13. try {
  14. b = read0(); //调用read0()方法
  15. } finally {
  16. IoTrace.fileReadEnd(traceContext, b == -1 ? 0 : 1); //从Path读出b=read0()
  17. }
  18. return b;
  19. }
  20. /**
  21. * 从输入流中读取多个字节到byte数组中
  22. * 该方法也是私有本地方法,不对用户开放,只供内部调用。
  23. */
  24. private native int readBytes(byte b[], int off, int len) throws IOException;
  25. //调用native方法readBytes(b, 0, b.length)每次读取多个字节
  26. public int read(byte b[]) throws IOException {
  27. Object traceContext = IoTrace.fileReadBegin(path);
  28. int bytesRead = 0;
  29. try {
  30. bytesRead = readBytes(b, 0, b.length); //每次读取缓冲区全部的
  31. } finally {
  32. IoTrace.fileReadEnd(traceContext, bytesRead == -1 ? 0 : bytesRead);
  33. }
  34. return bytesRead;
  35. }
  36. }

BufferedInputStream

  1. package java.io;
  2. public class BufferedInputStream extends FilterInputStream {
  3. private static int defaultBufferSize = 8192; //缓冲区数组默认大小8192Byte,也就是8K
  4. protected volatile byte buf[]; //内部缓冲数组,会根据需要进行填充,大小默认为8192字节,也可以用构造函数自定义大小
  5. protected int count; //当count=0时,说明缓冲区内容已读完,会再次填充
  6. protected int pos; // 缓冲区指针,记录缓冲区当前读取位置
  7. private InputStream getInIfOpen() throws IOException {
  8. InputStream input = in;
  9. if (input == null)
  10. throw new IOException("Stream closed");
  11. return input;
  12. }
  13. //创建空缓冲区
  14. private byte[] getBufIfOpen() throws IOException {
  15. byte[] buffer = buf;
  16. if (buffer == null)
  17. throw new IOException("Stream closed");
  18. return buffer;
  19. }
  20. //创建默认大小的BufferedInputStream
  21. public BufferedInputStream(InputStream in) {
  22. this(in, defaultBufferSize);
  23. }
  24. //此构造方法可以自定义缓冲区大小
  25. public BufferedInputStream(InputStream in, int size) {
  26. super(in);
  27. if (size <= 0) {
  28. throw new IllegalArgumentException("Buffer size <= 0");
  29. }
  30. buf = new byte[size];
  31. }
  32. private void fill() throws IOException {
  33. byte[] buffer = getBufIfOpen();
  34. if (markpos < 0)
  35. pos = 0;
  36. //....部分源码省略
  37. count = pos;
  38. int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
  39. if (n > 0)
  40. count = n + pos;
  41. }
  42. /**
  43. * 读取一个字节
  44. * 与FileInputStream中的read()方法不同的是,这里是从缓冲区数组中读取了一个字节
  45. * 也就是直接从内存中获取的,效率远高于前者
  46. */
  47. public synchronized int read() throws IOException {
  48. if (pos >= count) {
  49. fill();
  50. if (pos >= count)
  51. return -1;
  52. }
  53. return getBufIfOpen()[pos++] & 0xff;
  54. }
  55. //从缓冲区中一次读取多个字节
  56. private int read1(byte[] b, int off, int len) throws IOException {
  57. int avail = count - pos;
  58. if (avail <= 0) {
  59. if (len >= getBufIfOpen().length && markpos < 0) {
  60. return getInIfOpen().read(b, off, len);
  61. }
  62. fill();
  63. avail = count - pos;
  64. if (avail <= 0) return -1;
  65. }
  66. int cnt = (avail < len) ? avail : len;
  67. System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
  68. pos += cnt;
  69. return cnt;
  70. }
  71. public synchronized int read(byte b[], int off, int len){
  72. //为减少文章篇幅,源码就不显示了
  73. }
  74. }

InputStream:通过源码可以看到,如果用read()方法读取一个文件,每读取一个字节就要访问一次硬盘,这种读取的方式效率是很低的。即便使用read(byte b[])方法一次读取多个字节,每次读取多个字节就要访问一次硬盘。当读取的文件较大时,也会频繁的对磁盘操作。
BufferedInputStream:也就是说,Buffered类初始化时会创建一个较大的byte数组,一次性从底层输入流中读取多个字节来填充byte数组,当程序读取一个或多个字节时,可直接从byte数组中获取,当内存中的byte读取完后,会再次用底层输入流填充缓冲区数组。这种从直接内存中读取数据的方式要比每次都访问磁盘的效率高很多。

总结-结论

  • InputStream每次read,都需要从硬件读取文件一个或者若干个字节。
  • BufferedInputStream会提前读取默认大小字节到内部缓冲区,每次read都直接从内存读取一个或若干个字节。