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;

  1. 可以看到,read方法的实现最终调用的是native表示的本地方法,数据IO的工作并不是由InputStream自身完成 的。再看方法描述可以发现:
  2. - 调用该方法时,如果没有数据可读,方法会一直阻塞
  3. - 读取时按照逐字节的方式读取,如果读取完毕则返回-1
  4. NIO调用read方法时不会阻塞,有就读,没有就返回
  5. - **Java io没有NIO中选择器的实现**:NIO需要底层操作系统的系统调用支持
  6. ---
  7. <a name="OxBa3"></a>
  8. ### 1. Overview
  9. Java中的NIO主要包括三个核心概念:
  10. - **Channels**:管道
  11. - **Buffers**:缓冲区
  12. - **Selectors **:选择器
  13. 当然nio中还有很多其他的类和成分,但是上述的三个是其他所有实现的核心,其他的实现更像是一些工具类,用于粘合ChannelsBuffersSelectors
  14. 其中Channel可以看做是一个流,程序可以从Channel中读取数据写入到Buffer,也可以从Buffer中读取数据写入到Channel
  15. ![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)
  16. nioChannel的主要类型有网络IO和文件IO,相应的实现类主要有:
  17. - **FileChannel**
  18. - **DatagramChannel**
  19. - **SocketChannel**
  20. - **ServerSocketChannel**
  21. nioBuffer的主要实现有:
  22. - ByteBuffer
  23. - CharBuffer
  24. - DoubleBUffer
  25. - FloatBuffer
  26. - IntBuffer
  27. - LongBUffer
  28. - ShortBuffer
  29. 它对于Java中不同类型的数据都提供了相应的实现类。
  30. Selector允许单线程同时处理多个Channel,它主要应用于:当程序存在多个连接,但每个连接传输的数据很少,使用Selector可以减少资源消耗,提高处理的效率。例如,如下是针对于3ChannelSelector:<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)
  31. Selector可以通过调用select方法管理注册的所有Channel,该方法会一直阻塞,直到其中的一个Channel对应的事件发生。当事件返回时,Selector对应的线程便可以处理返回的事件。
  32. ---
  33. <a name="mo0fn"></a>
  34. ### 2. Channel
  35. Channel和通常所说的流(Stream)有相似的地方,但也有不同的地方,例如:
  36. - Channel是**双向**的,既可以向Channel中写入数据,也可以从Channel中读取数据。而Streams通常是**单向**的,如输入流、输出流
  37. - Channel支持**异步的读取和写入**数据
  38. - Channel通常和Buffer一起来实现数据的读取和写入
  39. Channel的常用实现类有:
  40. - **FileChannel:**支持从文件中读取数据
  41. - **DatagramChannel:**支持通过UDP来读取和写入数据
  42. - **SocketChannel:**支持通过TCP来读取和写入数据
  43. - **ServerSocketChannel:**支持监听连接的TCP连接,每一个连接对应一个SocketChannel
  44. 例如,我们使用FileChannel来读取文件中的内容,如下所示:
  45. ```java
  46. /**
  47. * @Author dyliang
  48. * @Date 2020/10/25 9:17
  49. * @Version 1.0
  50. */
  51. public class ChannelDemo {
  52. public static void main(String[] args) throws IOException {
  53. RandomAccessFile file = new RandomAccessFile("test.txt", "rw");
  54. FileChannel channel = file.getChannel();
  55. ByteBuffer buffer = ByteBuffer.allocate(48);
  56. int bytesRead = channel.read(buffer);
  57. while(bytesRead != -1){
  58. System.out.println("Read " + bytesRead);
  59. buffer.flip();
  60. while(buffer.hasRemaining()){
  61. System.out.println((char) buffer.get());
  62. }
  63. buffer.clear();
  64. bytesRead = channel.read(buffer);
  65. System.out.println(bytesRead);
  66. }
  67. file.close();
  68. }
  69. }

上面的例子实现的是Buffer和Channel之间的数据传输,另外,Channel还提供了transferTo方法和transferFrom方法用于Channel之间直接的数据传输。例如:

  1. RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
  2. FileChannel fromChannel = fromFile.getChannel();
  3. RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
  4. FileChannel toChannel = toFile.getChannel();
  5. long position = 0;
  6. long count = fromChannel.size();
  7. toChannel.transferFrom(fromChannel, position, count);

另外,一些SocketChannel实现可能只传输SocketChannel已经在它的内部缓冲区中准备好的数据,即使SocketChannel以后可能有更多可用的数据。因此,它可能不会将所请求的全部数据(计数)从SocketChannel传输到FileChannel。

transferTo方法和上面的transferFrom用法类似,不同之处在于方法调用的形式不同:

  1. 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的起始位置,后续写入的数据将在它之后保存

例如:

  1. ByteBuffer buffer = ByteBuffer.allocate(48);
  2. int bytesRead = channel.read(buffer); // 向buffer写入数据
  3. while(bytesRead != -1){
  4. System.out.println("Read " + bytesRead);
  5. buffer.flip(); // 转换为读模式
  6. while(buffer.hasRemaining()){
  7. System.out.println((char) buffer.get()); // 获取数据
  8. }
  9. buffer.clear(); // 清空buffer内存空间

Buffer本质上就是一块可以重复进行读写的内存空间,为了理解它是如何使用内存来进行读写,需要理解如下的三个概念:

  • capacity:容量,缓冲区的总长度,如果缓冲区已满还需要写入数据,就需要先清空再写入
  • position:位置,下一个要操作的数据元素的位置。起始位置为0,随着数据的写入不断的后移,最大为capacity - 1。当从buffer中读取数据时,position重置回0,记录下一个要读取数据的位置
  • limit:缓冲区中不可操作的下一个元素的位置,用于限制程序可以写入或者读取的数据量,通常为limit <=capacity

    读取的数据量不能超过写入的数据量。

  • mark:用于暂存position的值,便于重复使用position

image.png

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时,实际上做的工作如下:

  1. public final Buffer clear() {
  2. position = 0; // 将position置为0,下次直接从头开始写入数据
  3. limit = capacity; // limit和capacity一致
  4. mark = -1; // mark置为-1
  5. return this;
  6. }

4. Scater、Gather

nio提供了scatter(分散)和gather(收集)的支持,用于向Channel写入数据或者从Channel读取数据,用于需要单独处理多个部分数据的场景。scatter用于将从Channel中读取的数据写入到多个Buffer之中,gather用于将多个Buffer中的数据写入到一个Channel之中。

scatter用来从单个Channel读取数据到多个buffer中,如下所示:
image.png

例如,将Channel中的数据写入到两个ByteBuffer中:

  1. ByteBuffer header = ByteBuffer.allocate(128);
  2. ByteBuffer body = ByteBuffer.allocate(1024);
  3. ByteBuffer[] bufferArray = { header, body };
  4. channel.read(bufferArray);

scatter按照数组的顺序向buffer中写入数据,一旦buffer被写满,它将移动到另一个buffer执行写操作,不适于动态的调整写入数据的大小。

gather用于将多个buffer中的数据写入到Channel中,如下所示:
image.png

例如:

  1. ByteBuffer header = ByteBuffer.allocate(128);
  2. ByteBuffer body = ByteBuffer.allocate(1024);
  3. //write data into buffers
  4. ByteBuffer[] bufferArray = { header, body };
  5. channel.write(bufferArray);

此时,只有在缓冲区的位置和限制之间的数据被写入。例如,如果缓冲区的容量为128字节,但只包含58字节,则只有58字节从该缓冲区写入到Channel。


5. Selector

Selector支持单线程来管理多个Channel,用于检测管理的Channel中哪些准备好执行读操作,或是写操作。如果不使用Selector管理Channel,那么每个Channel都需要单独的一个线程。线程需要占用一定的资源,并且线程上下文的切换开销很大。因此,Selector通过支持单线程来管理多个Channel,减少了线程的使用,充分利用硬件的多核性能,从而降低了性能开销。

NIO - 图4

下面,我们通过程序看一下如何使用Selector。首先,创建一个Selector:

  1. Selector selector = Selector.open();

调用register方法向Selector中注册需要管理的Channel:

  1. channel.configureBlocking(false); // Channel此时必须是非阻塞类型
  2. 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方法获取到对应的事件标号,如下所示:

  1. int interestSet = selectionKey.interestOps();
  2. boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
  3. boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
  4. boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
  5. boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

SelectionKey的定义中,不同的事件通过不同的数字表示,所以可以通过与运算来判断监听的是哪个事件。

  1. public abstract class SelectionKey {
  2. public static final int OP_READ = 1;
  3. public static final int OP_WRITE = 4;
  4. public static final int OP_CONNECT = 8;
  5. public static final int OP_ACCEPT = 16;
  6. private volatile Object attachment = null;
  7. // 省略
  8. }

ready set表示Channel已经准备好执行的操作类型,通过调用readyOps方法获取对应的标识:

  1. int readySet = selectionKey.readyOps();

通过下面的四个方法可以判断Channel具体准备好执行操作的类型。

  1. selectionKey.isAcceptable();
  2. selectionKey.isConnectable();
  3. selectionKey.isReadable();
  4. selectionKey.isWritable();

另外,还可以通过方法来获取对应的Selector和Channel。

  1. Channel channel = selectionKey.channel();
  2. Selector selector = selectionKey.selector();

attach object是一个Object对象,它用于向SelectionKey附加一些额外需要的东西,如和Channel一起使用的Buffer,或者任何想要附加的东西。

  1. selectionKey.attach(theObject);
  2. Object attachedObj = selectionKey.attachment();

另外,在前面的register方法中也可以将attach object通过参数传入进行操作。

  1. 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的集合。

  1. Set<SelectionKey> selectedKeys = selector.selectedKeys();

通过迭代遍历得到的集合就可以获取到这些准备好的Channel。

  1. Set<SelectionKey> selectedKeys = selector.selectedKeys();
  2. Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
  3. while(keyIterator.hasNext()) {
  4. SelectionKey key = keyIterator.next();
  5. // 通过SelectionKey.channel()获取对应的Channel
  6. if(key.isAcceptable()) {
  7. // a connection was accepted by a ServerSocketChannel.
  8. } else if (key.isConnectable()) {
  9. // a connection was established with a remote server.
  10. } else if (key.isReadable()) {
  11. // a channel is ready for reading
  12. } else if (key.isWritable()) {
  13. // a channel is ready for writing
  14. }
  15. keyIterator.remove();
  16. }

注意每次迭代结束时调用的keyIterator.remove方法。Selector不会从集合中移除SelectionKey实例。当处理完Channel时,必须手动的将其移除。等到下次Channel变为“ready”时,选择Selector将再次将其添加到集合中。

由于管理Selector的单线程在所有的Channel都没有准备好的情况下,它会一直阻塞。如果想要该线程立刻唤醒,可以通过另一个线程调用Selector.wakeUp方法实现。如果另一个线程调用了wakeup方法,并且当前在select方法中没有阻塞线程,那么下一个调用select方法的线程将立即唤醒。

一旦Selector使用完毕,可以调用Selector的close方法,所有注册的SelectionKey实例都将作废,但是管理的Channel并不会关闭。