Java NIO和Java io的区别?
- Java io是面向流的,NIO是面向缓冲区的:Java IO可以分为字节输入输出流和字符输入输出流,但它们都属于阻塞式IO(Blocking IO)的实现。程序只能以流式的方式顺序的从一个流中读取一个或多个字节,不能随意的改变读取指针的位置。NIO可以将Channel中的数据读取到Buffer中,也可以将Buffer中的数据写入到Channel中,可以随意的读取Buffer中任意位置的数据
- Java io是阻塞的,NIO是非阻塞的:以FileInputStream为例进行说明,首先看一下read方法的源码实现,如下所示: ```java /**
- Reads a byte of data from this input stream. This method blocks
- if no input is yet available. *
- @return the next byte of data, or
-1
if the end of the - file is reached.
- @exception IOException if an I/O error occurs. */ public int read() throws IOException { return read0(); }
private native int read0() throws IOException;
可以看到,read方法的实现最终调用的是native表示的本地方法,数据IO的工作并不是由InputStream自身完成 的。再看方法描述可以发现:
- 调用该方法时,如果没有数据可读,方法会一直阻塞
- 读取时按照逐字节的方式读取,如果读取完毕则返回-1
NIO调用read方法时不会阻塞,有就读,没有就返回
- **Java io没有NIO中选择器的实现**:NIO需要底层操作系统的系统调用支持
---
<a name="OxBa3"></a>
### 1. Overview
Java中的NIO主要包括三个核心概念:
- **Channels**:管道
- **Buffers**:缓冲区
- **Selectors **:选择器
当然nio中还有很多其他的类和成分,但是上述的三个是其他所有实现的核心,其他的实现更像是一些工具类,用于粘合Channels、Buffers和Selectors。
其中Channel可以看做是一个流,程序可以从Channel中读取数据写入到Buffer,也可以从Buffer中读取数据写入到Channel。
![image.png](https://cdn.nlark.com/yuque/0/2020/png/1567167/1603531121938-0aee9f56-bca2-4f24-818e-97c4733b41c9.png#align=left&display=inline&height=271&margin=%5Bobject%20Object%5D&name=image.png&originHeight=271&originWidth=744&size=21660&status=done&style=shadow&width=744)
nio中Channel的主要类型有网络IO和文件IO,相应的实现类主要有:
- **FileChannel**
- **DatagramChannel**
- **SocketChannel**
- **ServerSocketChannel**
nio中Buffer的主要实现有:
- ByteBuffer
- CharBuffer
- DoubleBUffer
- FloatBuffer
- IntBuffer
- LongBUffer
- ShortBuffer
它对于Java中不同类型的数据都提供了相应的实现类。
Selector允许单线程同时处理多个Channel,它主要应用于:当程序存在多个连接,但每个连接传输的数据很少,使用Selector可以减少资源消耗,提高处理的效率。例如,如下是针对于3个Channel的Selector:<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1567167/1603531567061-388c25c1-db98-46a1-89e7-da40181fdbfe.png#align=left&display=inline&height=447&margin=%5Bobject%20Object%5D&name=image.png&originHeight=447&originWidth=760&size=35567&status=done&style=shadow&width=760)
Selector可以通过调用select方法管理注册的所有Channel,该方法会一直阻塞,直到其中的一个Channel对应的事件发生。当事件返回时,Selector对应的线程便可以处理返回的事件。
---
<a name="mo0fn"></a>
### 2. Channel
Channel和通常所说的流(Stream)有相似的地方,但也有不同的地方,例如:
- Channel是**双向**的,既可以向Channel中写入数据,也可以从Channel中读取数据。而Streams通常是**单向**的,如输入流、输出流
- Channel支持**异步的读取和写入**数据
- Channel通常和Buffer一起来实现数据的读取和写入
Channel的常用实现类有:
- **FileChannel:**支持从文件中读取数据
- **DatagramChannel:**支持通过UDP来读取和写入数据
- **SocketChannel:**支持通过TCP来读取和写入数据
- **ServerSocketChannel:**支持监听连接的TCP连接,每一个连接对应一个SocketChannel
例如,我们使用FileChannel来读取文件中的内容,如下所示:
```java
/**
* @Author dyliang
* @Date 2020/10/25 9:17
* @Version 1.0
*/
public class ChannelDemo {
public static void main(String[] args) throws IOException {
RandomAccessFile file = new RandomAccessFile("test.txt", "rw");
FileChannel channel = file.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = channel.read(buffer);
while(bytesRead != -1){
System.out.println("Read " + bytesRead);
buffer.flip();
while(buffer.hasRemaining()){
System.out.println((char) buffer.get());
}
buffer.clear();
bytesRead = channel.read(buffer);
System.out.println(bytesRead);
}
file.close();
}
}
上面的例子实现的是Buffer和Channel之间的数据传输,另外,Channel还提供了transferTo方法和transferFrom方法用于Channel之间直接的数据传输。例如:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel, position, count);
另外,一些SocketChannel实现可能只传输SocketChannel已经在它的内部缓冲区中准备好的数据,即使SocketChannel以后可能有更多可用的数据。因此,它可能不会将所请求的全部数据(计数)从SocketChannel传输到FileChannel。
transferTo方法和上面的transferFrom用法类似,不同之处在于方法调用的形式不同:
fromChannel.transferTo(position, count, toChannel);
同样的,SocketChannel调用transferTo方法时,只能转移和它缓冲区容量一致的数据。
3. Buffer
Buffer常和Channel配合使用,数据可以从Buffer写入到Channel中,同样可以从Channel中获取数据写入到Buffer中。Buffer本质上是一块可以读写数据的内存空间,它被包装成NIO中的Buffer对象,并且提供了一系列的方法使得程序可以轻松的操作这部分内存空间。
Buffer类不是线程安全的。
通过Buffer来读写数据主要分为如下的四步:
- 向Buffer中写入数据
- 调用buffer.flip()方法
- 从Buffer中读取数据
- 调用buffer.clear()或者buffer.compact()来回收buffer空间
当程序向Buffer中写入数据时,buffer会持续记录写入的数据量。当需要从Buffer中读取数据时,需要调用flip方法将buffer从写模式转换为读模式,然后可以读取buffer中所有的数据。另外,一旦读取数据完毕,程序需要清空buffer,便于后续其他数据的写入。NIO中提供了两种方式来实现:
- clear方法:清空,它将清空整块的buffer空间
- compact方法:压缩整理,它只清空已经被读取的数据所占的内存空间,剩下还没有被读取的数据将移动到buffer的起始位置,后续写入的数据将在它之后保存
例如:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = channel.read(buffer); // 向buffer写入数据
while(bytesRead != -1){
System.out.println("Read " + bytesRead);
buffer.flip(); // 转换为读模式
while(buffer.hasRemaining()){
System.out.println((char) buffer.get()); // 获取数据
}
buffer.clear(); // 清空buffer内存空间
}
Buffer本质上就是一块可以重复进行读写的内存空间,为了理解它是如何使用内存来进行读写,需要理解如下的三个概念:
- capacity:容量,缓冲区的总长度,如果缓冲区已满还需要写入数据,就需要先清空再写入
- position:位置,下一个要操作的数据元素的位置。起始位置为0,随着数据的写入不断的后移,最大为capacity - 1。当从buffer中读取数据时,position重置回0,记录下一个要读取数据的位置
limit:缓冲区中不可操作的下一个元素的位置,用于限制程序可以写入或者读取的数据量,通常为limit <=capacity
读取的数据量不能超过写入的数据量。
mark:用于暂存position的值,便于重复使用position
Buffer的常用实现有:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
- MappedByteBuffer:用于内存映射的ByteBuffer
针对于不同类型的数据可以选择相应的实现使用。Buffer常用的方法有:
方法名 | 描述 |
---|---|
allocate | 用于获取指定容量和类型的Buffer对象,例如:ByteBuffer buf = ByteBuffer.allocate(48); |
向Buffer写入数据 | nio提供了两种方法向buffer中写入数据: - 从Channel获取数据写入,例如:int bytesRead = inChannel.read(buf) - 自行写入:buf.put(11) |
flip | 将Buffer从写模式转换为读模式,将position重置回0,limit标记写入的数据量 |
从Buffer读取数据 | nio提供了两种方法来从Buffer读取数据: - 读取数据写入Channel:int WriteCount = channel.write(buf) - 自行读取:buf.get() |
rewind | 用于将position重置为0,便于重新读取数据 |
clear | 清空Buffer内存空间 |
compact | 它只清空已经被读取的数据所占的内存空间,剩下还没有被读取的数据将移动到buffer的起始位置,后续写入的数据将在它之后保存 |
mark | 标记缓冲区中的给定位置 |
reset | 将位置重置回标记的位置 |
equals | 用于比较两个Buffer的内容,两个Buffer的内容相等需要满足: - 类型相同 - 剩余的数据量相同 - 剩余的数据内容相同 |
compareTo | 以词典顺序进行比较,它在缓冲区参数小于、等于或者大于引用 compareTo( )的对象实例时,分别返回一个负整数,0 和正整数 |
其中,clear方法和compact方法清空Buffer时,并不表示Buffer内的数据被清空。原来的数据相当于是被遗忘了,程序无法再次读取buffer中的数据,后续写入的新数据会直接覆盖掉旧数据。这涉及到另一个概念mark, 它用于记录当前position的前一个位置或者默认是-1。当调用clear方法清空buffer时,实际上做的工作如下:
public final Buffer clear() {
position = 0; // 将position置为0,下次直接从头开始写入数据
limit = capacity; // limit和capacity一致
mark = -1; // mark置为-1
return this;
}
4. Scater、Gather
nio提供了scatter(分散)和gather(收集)的支持,用于向Channel写入数据或者从Channel读取数据,用于需要单独处理多个部分数据的场景。scatter用于将从Channel中读取的数据写入到多个Buffer之中,gather用于将多个Buffer中的数据写入到一个Channel之中。
scatter用来从单个Channel读取数据到多个buffer中,如下所示:
例如,将Channel中的数据写入到两个ByteBuffer中:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
scatter按照数组的顺序向buffer中写入数据,一旦buffer被写满,它将移动到另一个buffer执行写操作,不适于动态的调整写入数据的大小。
gather用于将多个buffer中的数据写入到Channel中,如下所示:
例如:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
此时,只有在缓冲区的位置和限制之间的数据被写入。例如,如果缓冲区的容量为128字节,但只包含58字节,则只有58字节从该缓冲区写入到Channel。
5. Selector
Selector支持单线程来管理多个Channel,用于检测管理的Channel中哪些准备好执行读操作,或是写操作。如果不使用Selector管理Channel,那么每个Channel都需要单独的一个线程。线程需要占用一定的资源,并且线程上下文的切换开销很大。因此,Selector通过支持单线程来管理多个Channel,减少了线程的使用,充分利用硬件的多核性能,从而降低了性能开销。
下面,我们通过程序看一下如何使用Selector。首先,创建一个Selector:
Selector selector = Selector.open();
调用register方法向Selector中注册需要管理的Channel:
channel.configureBlocking(false); // Channel此时必须是非阻塞类型
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
其中register方法的最后一个参数是interest set,它表示程序想要通过Selector监听Channel中发生的感兴趣的事件类型。它有四个取值:
- Connect
- Accept
- Read
- Write
如果一个Channel成功的连接到server称为connect ready;如果server的socket channel接收了一个连接,称为accept ready;如果一个Channel准备好读取数据,称为read ready;如果一个Channel准备好写数据,称为write ready。
SelectionKey的四个取值表示上述的四种事件:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
如果感兴趣多种类型的事件,可以使用 | 进行组合。
其中register方法会返回一个SelectionKey对象,它包含如下几方面的信息:
- interest set
- ready set
- Channel
- Selector
- an attached object
其中interest set就是前面调用register方法时绑定的想要监听的事件,通过interestOps方法获取到对应的事件标号,如下所示:
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
SelectionKey的定义中,不同的事件通过不同的数字表示,所以可以通过与运算来判断监听的是哪个事件。
public abstract class SelectionKey {
public static final int OP_READ = 1;
public static final int OP_WRITE = 4;
public static final int OP_CONNECT = 8;
public static final int OP_ACCEPT = 16;
private volatile Object attachment = null;
// 省略
}
ready set表示Channel已经准备好执行的操作类型,通过调用readyOps方法获取对应的标识:
int readySet = selectionKey.readyOps();
通过下面的四个方法可以判断Channel具体准备好执行操作的类型。
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
另外,还可以通过方法来获取对应的Selector和Channel。
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
attach object是一个Object对象,它用于向SelectionKey附加一些额外需要的东西,如和Channel一起使用的Buffer,或者任何想要附加的东西。
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
另外,在前面的register方法中也可以将attach object通过参数传入进行操作。
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
一旦在Selector中注册好了多个Channel,已经对每个Channel感兴趣的事件进行了监听,就可以调用select方法。当注册的多个Channel中有一些Channel中监听的事件发生了,那么就会返回这些Channel。其中,select方法如下的三种形式:
- int select():方法阻塞直到至少一个Channel准备好了对应的事件操作
- int select(long timeOut):类似于上者,不过增加了一个最长的等待时间,不是一直阻塞
- int selectNow():没有发现满足的Channel就立即返回,不阻塞
返回值表示准备好的Channel的个数。
一旦通过select方法返回了一个或多个Channel,可以调用selectKeys方法来获取对应的selectionKey的集合。
Set<SelectionKey> selectedKeys = selector.selectedKeys();
通过迭代遍历得到的集合就可以获取到这些准备好的Channel。
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 通过SelectionKey.channel()获取对应的Channel
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
注意每次迭代结束时调用的keyIterator.remove方法。Selector不会从集合中移除SelectionKey实例。当处理完Channel时,必须手动的将其移除。等到下次Channel变为“ready”时,选择Selector将再次将其添加到集合中。
由于管理Selector的单线程在所有的Channel都没有准备好的情况下,它会一直阻塞。如果想要该线程立刻唤醒,可以通过另一个线程调用Selector.wakeUp方法实现。如果另一个线程调用了wakeup方法,并且当前在select方法中没有阻塞线程,那么下一个调用select方法的线程将立即唤醒。
一旦Selector使用完毕,可以调用Selector的close方法,所有注册的SelectionKey实例都将作废,但是管理的Channel并不会关闭。