java成长笔记

java input/output速度对比

2019-08-02  本文已影响0人  G_uest

说明

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

继承关系

生成测试文件

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() 方法,速度反而会慢,因为多了一步刷新缓存的步骤。

上一篇下一篇

猜你喜欢

热点阅读