什么是NIO
有的人喜欢称作New I/O,原因相对于原来的I/O方式属于新增I/O。也有人称为Non-block I/O(非阻塞I/O),NIO的目标也是让Java拥有非阻塞的I/O,所以就叫它非阻塞的I/O,吼吼。
基本概念
NIO比较核心的三个部分:Buffer,Channel,Selectors。
缓冲区(Buffer)
在面向流的I/O中,可以将数据直接写入,或者读到Stream中。在NIO库中,所有数据都是用缓冲区来处理,在读取数据时直接读到缓冲区,在写入数据时,直接写到缓冲区。可以想象成水管往水池里面蓄水,一旦有数据写入,就像水流一样流到水池里面暂时存储。Buffer的实现有以下几种:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
涵盖了io发送的基础数据类型:byte, short, int, long, float, double 和 char。
Buffer的三个属性:
capacity,position,limit,capacity是指定buffer的容量,那么position和limit呢?它们的含义主要取决于buffer是出于读模式还是写模式,对于capacity来说都一样,你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。而position在写数据到buffer时,position表示当前的位置,每一次写入数据position都会移动到下一个位置(最大为capacity-1),当读数据时是从某个位置特定的读,从写模式切换到读模式时position会被重置为0,从buffer读数据时,position会移动到下一个可读的位置。至于limit,在写模式下表示你可以最多写入多少数据,limit等于capacity,切换到读模式时,limit等于写模式的position,也就是你写入了多少,你最大可以读多少。
那么如何分配Buffer?(以ByteBuffer为例)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
跟到源码中看一下:allocate是一个静态方法,而ByteBuffer是一个抽象类,最后返回了一个实现类HeapByteBuffer实例
public static ByteBuffer allocate(int capacity) {
if (capacity < 0) throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
而HeapByteBuffer的构造函数传入了两个capacity,为什么呢?进去看一下就知道了
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
刚刚有一个capacity是当作limit的初始值赋予给limit了,也就是上面提到过的写模式下limit的值是等于capacity的。从这可以看到HeapByteBuffer底层存储使用的是byte数组,而HeapByteBuffer是在堆上实现的ByteBuffer,还有一个实现类是DirectByteBuffer,前面的HeapByteBuffer的内存分配是在jvm之内,而DirectByteBuffer得内存分配是在Jvm之外,使用的是系统本地内存,分配方法用的是c的malloc。
Channel
原话是这么说的:
Java NIO Channels are similar to streams with a few differences:
- You can both read and write to a Channels. Streams are typically
- one-way (read or write). Channels can be read and written
- asynchronously. Channels always read to, or write from, a Buffer.
Java NIO类似流,但又有些不同:
- 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
- 通道可以异步地读写。
- 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
Channel用法:
RandomAccessFile aFile = new RandomAccessFile("E:\\test.txt", "rw");
// 获取文件通道
FileChannel inChannel = aFile.getChannel();
// 申请一个48字节的空间
ByteBuffer buf = ByteBuffer.allocate(48);
// 向Buffer中写入
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
注意 buf.flip() 的调用,首先读取数据到Buffer,然后反转Buffer,接着再从Buffer中读取数据。
Selector
Selector允许单线程处理多个channel,使用selector得向它注册channel,然后调用select方法,这个方法会阻塞到某个通道有事件就绪,一旦这个方法返回,线程就可以处理了。
创建一个selector:
Selector selector = Selector.open();
向selector注册一个channel:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
值得注意的是和selector一起使用channel必须设置为非阻塞模式,所以FileChannel不可以与selector一起使用,FileChannel不能切换到非阻塞模式。虽好与Socket Channels一起使用。在注册channel时加入了一个事件SelectionKey.OP_READ,意味着对这个通道监听得事件是读事件,一旦通道中有数据写入,select方法就会返回。当然还有其他的事件:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
对应着connect,accept,read,write,四个事件。如果想要注册多个事件可以这样写:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
好了,NIO得基本概念算是说了一点,后面有时间继续补充吧。