美文网首页java成长笔记
java input/output速度对比

java input/output速度对比

作者: G_uest | 来源:发表于2019-08-02 23:15 被阅读0次

    说明

    本篇文章讨论自定义数组大小,对不同IO读写方法速度的影响
    读写速度受计算机软硬件环境影响,测试结果为多次测试数据,去除最大值和最小值,求平均值得出。

    继承关系

    • java.lang.Object
      • java.io.OutputStream
        • java.io.FilterOutputStream
          • java.io.BufferedOutputStream
      • java.io.InputStream
        • java.io.FilterInputStream
          • java.io.BufferedInputStream
      • java.io.Reader
        • java.io.BufferedReader
        • java.io.InputStreamReader
          • java.io.FileReader
      • java.io.Writer
        • java.io.BufferedWriter
        • java.io.OutputStreamWriter
          • java.io.FileWriter

    生成测试文件

    BufferedWriter bw = new BufferedWriter(fw);
    String str = "从前有座,山上有座庙,庙里有个老和尚,在和小和尚讲故事,讲的故事是:";
    for (int i = 0; i < 10000000; i++) {
        fw.write(str);
    }
    

    测试文本大小:972MB

    字节流

    代码

    public static void main(String[] args) {
        File file = new File("src/io/a.txt");
        File file1 = new File("src/io/test.txt");
        if (!file.exists()) {
            System.out.println("file is not found");
            return;
        }
        
        long start = System.currentTimeMillis();
        try {
            InputStream bis = new FileInputStream(file);
            OutputStream bos = new FileOutputStream(file1);
            
            // InputStream ins = new FileInputStream(file);
            // BufferedInputStream bis = new BufferedInputStream(ins);
            // OutputStream fos = new FileOutputStream(file1);
            // BufferedOutputStream bos = new BufferedOutputStream(fos);
            
            // 每次写入byte[1024]大的数据,其实这里就使用了缓存的思想  
            byte[] bytes = new byte[1024];
            int len = -1 ;
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }
            // BufferedOutputStream的close方法,将关闭此输入流并释放与流相关联的 任何 系统资源。 
            bis.close();
            bos.close();
            // 上边已经能关闭了流,下面这句会抛出IOException: Stream Closed
            // System.out.println(ins.read());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println(end -  start);
    }
    

    结果

    byte[]长度 FileInputStream
    FileOutputStream
    BufferedInputStream
    BufferedOutputStream
    1024 7953 ms 5840 ms
    8192 5193 ms 5458 ms
    102400 4820 ms 4904 ms

    结论

    BufferedOutputStream默认缓冲区是一个 byte[8192] 的数组。

    每次写入数据,如果数据小于8k,数据将保存到缓冲 buf 中,不进行磁盘io,一直到缓冲 buf 满了之后,才进行一次磁盘IO,把缓冲buf的字节全部写入磁盘。

    如果数据大于8K,先把刷新缓冲区,把缓冲区内容写入到磁盘,然后把传入的数据写入到磁盘。(此过程两次IO)
    如果每次传入数据大于等于8k,使用BufferedOutputStream的效率反而会慢,因为每次写入数据都多了一步——刷新缓存区

    BufferedOutputStream.class 部分源码:

    // 存储数据的内部缓冲区
    protected byte buf[];
    
    // 创建一个新的缓冲输出流来将数据写入指定底层输出流
    public BufferedOutputStream(OutputStream out) {
        // 这里将调用下面的 BufferedOutputStream 构造方法
        this(out, 8192);
    }
    
    // 创建一个新的缓冲输出流,来将数据写入,使用指定的缓冲区,指定底层输出流大小。
    public BufferedOutputStream(OutputStream out, int size) {
        super(out);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        // 指定 buf 数组的大小
        buf = new byte[size];
    }
    
    /** 
    *该方法将给定数组中的字节存储到这个数组流的缓冲区中,将刷新缓冲区到底层输出流
    *如果请求的长度大于等于此流的长度,该方法将刷新缓冲区并直接把字节写入到底层输出流。
    */
    public synchronized void write(byte b[], int off, int len) throws IOException {
          // 如果请求长度超过输出缓冲区的大小
          if (len >= buf.length) {
              // 刷新输出缓冲区,然后直接写入数据。
              flushBuffer();
              out.write(b, off, len);
              return;
          }
          if (len > buf.length - count) {
              flushBuffer();
          }
          System.arraycopy(b, off, buf, count, len);
          count += len;
    }
    
    /** 刷新内部缓冲区 */
    private void flushBuffer() throws IOException {
          // 如果缓存区中有数据,count:缓冲区中有效字节数
          if (count > 0) {
              out.write(buf, 0, count);
              count = 0;
          }
    }
    

    字符流

    代码

    public static void main(String[] args) {
        File file = new File("src/io/a.txt");
        File file1 = new File("src/io/test.txt");
        if (!file.exists()) {
            System.out.println("file is not found");
            return;
        }
        
        long start = System.currentTimeMillis();
        try {
            Reader br = new FileReader(file);
            Writer bw = new FileWriter(file1);
    //      BufferedReader br = new BufferedReader(fr);
    //      BufferedWriter bw = new BufferedWriter(fw);
            
            // 这里是字符数组
            char[] ch = new char[1024];
            int len = -1;
            while ((len = br.read(ch)) != -1) {
                bw.write(ch, 0, len);
            }
            bw.close();
            br.close();
            long end = System.currentTimeMillis();
            System.out.println("time:" + (end - start));
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

    结果

    char[]长度 FileReader
    FileWriter
    BufferedReader
    BufferedWriter
    1024 7153 ms 7056 ms
    8192 7067 ms 7254 ms
    102400 6826 ms 6979 ms

    结论

    FileWriter本来是没有缓存区的;

    API中说它有缓存,也是有原因的:
    FileWriter流中的构造函数调用的是父类OutputStreamWriter构造函数;
    而父类OutputStreamWriter构造函数本质是StreamEncoder类的forOutputStreamWriter方法。StreamEncoder中是有缓存区的。

    FileWriter执行写操作时,会调用父类OutputStreamWriter的方法,OutputStreamWriter的write方法在Writer类中;而Writer类的write方法是被StreamEncoder类实现的。
    调用过程:
      FileWriter --> OutputStreamWriter --> Writer --> StreamEncoder

    构造函数相互调用代码FileWriter extends OutputStreamWriter

    // FileWriter 构造方法
    public FileWriter(File file) throws IOException {
        super(new FileOutputStream(file));
    }
    
    // OutputStreamWriter 构造方法
    public OutputStreamWriter(OutputStream out) {
        super(out);
        try {
            se = StreamEncoder.forOutputStreamWriter(out, this, (String)null);
        } catch (UnsupportedEncodingException e) {
            throw new Error(e);
        }
    }
    

    FileWriter中没有任何实现方法,直接看他的父类
    OutputStreamWriter.class 部分源码:

    // StreamEncoder  字节字符转换流
    private final StreamEncoder se;
    
    /** 写入字符数组 */
    public void write(char cbuf[], int off, int len) throws IOException {
        // 这里调用了se的write方法,看下面StreamEncoder的源码
        se.write(cbuf, off, len);
    }
    
    

    StreamEncoder 部分源码:

    // 是否保存左字符标志,为了保证读入的字符不乱码 则每次读入不能少于两个字符
    private boolean haveLeftoverChar = false;
    // 默认字节缓冲区大小
    private static final int DEFAULT_BYTE_BUFFER_SIZE = 8192;
    // 字节缓冲区
    private ByteBuffer bb;
    
    void implWrite(char cbuf[], int off, int len) throws IOException {
        // 将字符序列包装到缓冲区中
        // 缓冲区的容量将为 cbuf.length(),其位置将为 off,其限额将为 len
        CharBuffer cb = CharBuffer.wrap(cbuf, off, len);
    
        // 为了保证读入的字符不乱码 则每次读入不能少于两个字符
        if (haveLeftoverChar)
        flushLeftoverChar(cb, false);
    
        // hasRemaining()方法用于判断字符缓冲区中是否还有元素
        while (cb.hasRemaining()) {
            // 从给定的缓冲区对象中,编码尽可能多的字符,把结果(字节)写入给定的输出缓冲区,并返回终止原因。
            // 参数false代表有可能提供其他输入
            // bb: 字节缓存区,private ByteBuffer bb;
            CoderResult cr = encoder.encode(cb, bb, false);
            if (cr.isUnderflow()) {
                assert (cb.remaining() <= 1) : cb.remaining();
                if (cb.remaining() == 1) {
                    haveLeftoverChar = true;
                    leftoverChar = cb.get();
            }
            break;
        }
        if (cr.isOverflow()) {
            assert bb.position() > 0;
            // 利用OutputStreamWriter对象,所使用到的底层字节输出流FileOutputStream,
            // 把字节缓冲区的内容给输出出去,然后清空字节流
            // 这里不再继续往下扒源码
            writeBytes();
            continue;
        }
        cr.throwException();
        }
    }
    
    
    public void write(char cbuf[], int off, int len) throws IOException {
        synchronized (lock) {
            // 确保流是打开的状态
            ensureOpen();
            if ((off < 0) || (off > cbuf.length) || (len < 0) ||
                ((off + len) > cbuf.length) || ((off + len) < 0)) {
                throw new IndexOutOfBoundsException();
            } else if (len == 0) {
                return;
            }
            implWrite(cbuf, off, len);
        }
    }
    

    BufferedWriter缓冲区是一个 char[8192] 数组。

    每次写入数据,如果数据小于8192个字符,数据将保存到缓冲 cb 中,不进行编码转换,一直到缓冲 cb 满了之后,才进行一次编码转换。

    如果数据大于8192个字符,先把刷新缓冲区,如果缓存区有内容,就把缓冲区内容进行一次编码转换,然后把传入的数据再进行编码转换。

    编码转换 StreamEncoder 类中也有缓存区,大小为8192。

    调用过程:
      BufferedWriter --> Writer --> StreamEncoder

    // BufferedWriter.class
    private char cb[];
    private static int defaultCharBufferSize = 8192;
    
    public void write(char cbuf[], int off, int len) throws IOException {
        synchronized (lock) {
            ensureOpen();
            if ((off < 0) || (off > cbuf.length) || (len < 0) ||
                ((off + len) > cbuf.length) || ((off + len) < 0)) {
                throw new IndexOutOfBoundsException();
            } else if (len == 0) {
                return;
            }
    
            if (len >= nChars) {
                // 如果请求长度超过输出缓冲区的大小,刷新缓冲区,然后直接写入数据
                flushBuffer();
                // 这里的 write
                out.write(cbuf, off, len);
                return;
            }
    
            int b = off, t = off + len;
            while (b < t) {
                int d = min(nChars - nextChar, t - b);
                System.arraycopy(cbuf, b, cb, nextChar, d);
                b += d;
                nextChar += d;
                if (nextChar >= nChars)
                    flushBuffer();
            }
        }
    }
    

    结论

    对于BufferedWriter和FileWriter在写入数据时,要进行的编码转换,在对比速度时可以忽略,因为二者都有这一步。
    如果自定义缓存大小(char[] / byte[]),大于 BufferedXxxx() 方法,默认的缓存大小(8192),使用 BufferedXxxx() 方法,速度反而会慢,因为多了一步刷新缓存的步骤。

    相关文章

      网友评论

        本文标题:java input/output速度对比

        本文链接:https://www.haomeiwen.com/subject/hdqddctx.html