持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情
1 NIO概述
1.1 阻塞IO
通常在进行同步 I/O 操作时,如果读取数据,代码会阻塞直至有可供读取的数据。同样,写入调用将会阻塞直至数据能够写入。
应用程序都是运行在用户空间的,所以它们能操作的数据也都在用户空间。按照这样子来理解,只要数据没有到达用户空间,用户线程就操作不了。
如果此时用户线程已经参与,那它一定会被阻塞在IO上。这就是常说的阻塞IO。用户线程被阻塞在等待数据上或拷贝数据上。
非阻塞IO就是用户线程不参与以上两个过程,即数据已经拷贝到用户空间后,才去通知用户线程,一上来就可以直接操作数据了。
用户线程没有因为IO的事情出现阻塞,这就是常说的非阻塞IO。
传统的Server/Client模式会基于TPR(Thread per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池线程的最大数量,这由带来了新的问题,如果线程池中有 100 个线程,而有100个用户都在进行大文件下载,会导致第 101 个用户的请求无法及时处理,即便第101 个用户只想请求一个几 KB 大小的页面。传统的 Server/Client 模式如下图所示:
1.2 非阻塞IO(NIO)
NIO 中非阻塞 I/O 采用了基于 Reactor 模式的工作方式,I/O 调用不会被阻塞,相反是注册感兴趣的特定 I/O 事件,如可读数据到达,新的套接字连接等等,在发生特定事件时,系统再通知用户。
NIO 中实现非阻塞 I/O 的核心对象就是Selector,Selector 就是注册各种 I/O 事件地方,而且当用户感兴趣的事件发生时,就是这个对象告诉我们所发生的事件,如下图所示:
从图中可以看出,当有读或写等任何注册的事件发生时,可以从 Selector 中获得相应的 SelectionKey,,同时从SelectionKey中可以找到发生的事件和该事件所发生的具体的SelectableChannel,以获得客户端发送过来的数据。
非阻塞指的是 IO 事件本身不阻塞,但是获取 IO 事件的 select()方法是需要阻塞等待的。区别是阻塞的 IO 会阻塞在 IO 操作上,NIO 阻塞在事件获取上,没有事件就没有 IO,从高层次看 IO 就不阻塞了。也就是说只有 IO 已经发生那么我们才评估 IO 是否阻塞,但是select()阻塞的时候 IO 还没有发生,何谈 IO 的阻塞呢?NIO 的本质是延迟 IO 操作到真正发生 IO 的时候,而不是以前的只要 IO 流打开了就一直等待 IO 操作。
1.3 NIO概述
Java NIO核心部分:
-
Channels
-
Buffer
-
Selectors
还有其他组件,比如Pipe(管道)和FileLock(文件锁),只不过是与三个核心组件共同使用的工具类。
1.3.1 Channel
首先说一下 Channel,可以翻译成“通道“。Channel和IO中的Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:InputStream,OutputStream,而Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。
NIO 中的 Channel 的主要实现有:FileChannel、DatagramChannel、SocketChannel和ServerSocketChannel,这里看名字就可以猜出个所以然来:分别可以对应文件IO、UDP 和 TCP(Server和Client)。
1.3.2 Buffer
NIO 中的关键 Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer,IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double,..float, int, long, short。
1.3.3 Selector
Selector 运行单线程处理多个 Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用Selector就会很方便。例如在一个聊天服务器中。要使用Selector,得向Selector 注册Channel,然后调用它的 select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件-事件的例子有如新的连接进来、数据接收等。
参考
https://www.cnblogs.com/lixinjie/p/a-post-about-io-clearly.html