这篇文章主要想总结下NIO的channel的传统io中的stream的差别在哪。网上找了很多文章,都感觉只是说了概念。然后自己大概看了下源码,结合概念,整理一下。有些地方可能不是很准确,也希望可以给点意见,互相学习。
这里不讲异步方面的东西,只是想单纯讲一下stream和channel在操作内存时的一些差异。
让我产生问题的来源主要有俩个:
1. 从概念上解读,stream是按照字节去处理的,看起来就像是水流一样,一个接一个。而channel是按照数据块来处理的。那么bufferedStream呢?加了buffer后是不是也是按照数据块来处理呢。那么这时,bufferedStream和channel的性能区别是在哪里呢?
2. 网上发现了一些文章,通过拷贝文件的实验来对比channel和stream的性能。stream使用的是bufferedStream。结果是channel的性能要比stream快1/3。当然我没有做实验去验证,只是通过代码的解读来理解下为什么性能会有这么大的差别。
首先需要引入一些背景知识,用户的线程是如何读入和写出文件的。下面这个图是从一本书上截下来的,简单的说明下读取文件时的流程。
1. 磁盘的controller把数据从磁盘拷贝到系统内核区。
2.然后cpu把数据从系统内核拷贝的用户的内存区。
3.系统对内存块的操作是按数据块操作的,这也是NIO的一个重要的概念,操作数据时尽量和操作系统相吻合,来提高内存操作的效率。
然后再分别分析一下channel和stream在操作内存时分别的步骤,就可以比较清晰的看出俩者的差别。
一 channel:
首先总结一下,这也是一个大家都知道的概念。channel的输入端和输出端都是byteBuffer。在内存操作的时候,也是以数据块为单位来进行数据移动。下面具体说下输出的步骤:
1. 如果我们使用的是directBuffer。 那么会直接调用native方法,把整块的内存写出到磁盘。
2.如果我们使用的是堆内的buffer,java会创建一个临时的directBuffer,把堆内buffer的数据拷贝到临时的 directBuffer。然后调用native方法把临时的 directBuffer的内容整块的写入磁盘。
下面贴一下源码来看一下:
static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException { if(var1 instanceof DirectBuffer) { // 如果是directBuffer,直接整块写入磁盘 return writeFromNativeBuffer(var0, var1, var2, var4); } else { int var5 = var1.position(); int var6 = var1.limit(); assert var5 <= var6; int var7 = var5 <= var6?var6 - var5:0; // 创建临时directBuffer ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7); int var10; try { var8.put(var1); var8.flip(); var1.position(var5); // 直接将临沭directBuffer整块写入磁盘 int var9 = writeFromNativeBuffer(var0, var8, var2, var4); if(var9 > 0) { var1.position(var5 + var9); } var10 = var9; } finally { Util.offerFirstTemporaryDirectBuffer(var8); } return var10; } }
二 stream
首先也说一下概念,stream是按照字节,一个一个操作内存的。但是,真的是这样的吗?
先看下FileOutputStream这个类:
public void write(byte b[], int off, int len) throws IOException { writeBytes(b, off, len, append); } private native void writeBytes(byte b[], int off, int len, boolean append) throws IOException;
可以看到,这里也是调用native方法,把整个byte块都写入了磁盘。具体native方法是怎么实现的,哪位大大可以帮忙分析一下?
再看下BufferedOutPutStream这个类,主要看下write方法和flushBuffer方法
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();
}
// 写入内部的buffer区
System.arraycopy(b, off, buf, count, len);
count += len;
}
/** Flush the internal buffer */ private void flushBuffer() throws IOException { if (count > 0) { 调用代理的outputStream的写出方法 out.write(buf, 0, count); count = 0; } }
所以,从这个角度来看,stream和channel都是直接把整个数据块对底层进行写入的。
那么,stream真的比channel慢吗? 值得怀疑。 哪位大佬可以帮忙解下惑呢?