本文的分析基于java8,jdk的版本为jdk1.8.0_91
。
什么是nio
概念
与oio的对比
典型代码
以文件的读取为例。这里不再赘述oio中文件读取过程,可以参考攻破JAVA NIO技术壁垒。
创建NIOTest类,假设file.txt
的内容为1234567890abcdefghijklmnopqrstuvwxy
, 利用Java的nio读取文件的简单代码如下:
public class NIOTest {
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocate(4);//①
Path path = Paths.get(System.getProperty("user.dir") + "/assets/file.txt");
FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);//②
int len = fileChannel.read(byteBuffer);//③
while (len != -1) {
byteBuffer.flip();//④
while (byteBuffer.hasRemaining()){
System.out.print((char) byteBuffer.get());//⑤
}
byteBuffer.clear();//⑥
len = fileChannel.read(byteBuffer);//⑦
}
}
}
①为byteBuffer
分配空间(这里使用的是HeapByteBuffer
);
②获取文件的fileChannel
(实际上是FileChannel
的一个实现类:FileChannelImpl
);
③从channel中读取数据到byteBuffer
;
④flip这个buffer(flip可以理解为反转,将buffer设置为一种可以进行读取的状态,具体见后文分析);
⑤从byteBuffer
中逐个获取byte;
⑥clear这个buffer(清空buffer,将buffer设置为一种可以重新从channel缓存数据的状态,具体见后文分析)
重复④至⑥步直到文件被读取完毕。
控制台显示出1234567890abcdefghijklmnopqrstuvwxy
Channel简单介绍
Buffer简单介绍
为什么要flip()
如果我们注释掉byteBuffer.flip();//④
,控制台输出为v
。
下面我们看一下flip()
的源码:
- 进入
java.nio.Buffer
类的flip()
方法:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
可以看到,flip做了三件事:把limit置为当前position的值,把position置零,把mark置为-1。源码里有这样一段描述:
Flips this buffer. The limit is set to the current position and then the position is set to zero. If the mark is defined then it is discarded.
flip将当前limit设为position的值,这样就限定了当前position之前的数据是可用的;然后把position置零,这样我们可以从头开始读取数据。所以需要先flip再读取数据。
为什么要clear()
如果我们注释掉byteBuffer.clear();//⑥
,控制台输出为123412341234
...一直循环,程序进入死循环。
为什么第2次及以后的channel.read需要先clear buffer?下面看一下源码:
- 进入
fileChannel.read(byteBuffer)
方法,实际上是进入了FileChannel
的实现类sun.nio.ch.FileChannelImpl
的public int read(ByteBuffer var1)
方法:
public int read(ByteBuffer var1) throws IOException {
this.ensureOpen();
if(!this.readable) {
throw new NonReadableChannelException();
} else {
Object var2 = this.positionLock;
synchronized(this.positionLock) {
int var3 = 0;
int var4 = -1;
byte var5;
try {
this.begin();
var4 = this.threads.add();
if(this.isOpen()) {
do {
var3 = IOUtil.read(this.fd, var1, -1L, this.nd);//①
} while(var3 == -3 && this.isOpen());
int var12 = IOStatus.normalize(var3);
return var12;
}
var5 = 0;
} finally {
this.threads.remove(var4);
this.end(var3 > 0);
assert IOStatus.check(var3);
}
return var5;
}
}
}
①关键步骤,进入IOUtil.read
中
-
IOUtil
代码片段:
static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
if(var1.isReadOnly()) {
throw new IllegalArgumentException("Read-only buffer");
} else if(var1 instanceof DirectBuffer) {
return readIntoNativeBuffer(var0, var1, var2, var4);
} else {
ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());//①
int var7;
try {
int var6 = readIntoNativeBuffer(var0, var5, var2, var4);
var5.flip();
if(var6 > 0) {
var1.put(var5);
}
var7 = var6;
} finally {
Util.offerFirstTemporaryDirectBuffer(var5);
}
return var7;
}
}
①根据var1(其实就是文章开头的NIOTest中的byteBuffer
)创建一个临时的ByteBuffer
:var5
。var5
的limit
属性与byteBuffer.remaining()有关
- 进入byteBuffer.remaining():
public final int remaining() {
return limit - position;
}
原来,如果不clear()
,这里的remaining则会是0,所以var5
的limit
就是0,这样len = fileChannel.read(byteBuffer);//⑦
的len就一直是0(不能从channel中读取到byteBuffer中),所以byteBuffer没有被覆盖,程序也陷入死循环。
Selector简单介绍
文件NIO的相关典型用法
Channel获取
从FileInputStream中获取
File file = new File(System.getProperty("user.dir") + "/assets/file.txt");
FileInputStream fileInputStream = new FileInputStream(file);
FileChannel fileChannel = fileInputStream.getChannel();
利用FileChannel获取
Path path = Paths.get(System.getProperty("user.dir") + "/assets/file.txt");
FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);
注意,Path是从JDK1.7开始的一个新的类。