原文:《Java NIO: Non-blocking Server》
GitHub 上的实例代码
https://github.com/jjenkov/java-nio-server
非阻塞IO管道
非阻塞IO管道是一系列组件的链接。简化结构如下(读写都适用):
Component 利用 Selector 来监测是否有 Channel 就绪可供读取数据;
Component 读取这些输入数据,并基于此生成输出数据;
最后将这些输出数据写到另一个 Channel 中。
非阻塞IO管道并不一定同时可读可写。它可以 只读 或 只写。
在实际项目中,一个管道可能会有对个对应的 Component 来处理输入的数据;管道的长度也会因业务需要而不同。
从 Channel 到 Selector,从 Selector 到 Component 的箭头其实是 Component 在处理 Selector 与 Channel,而不是 Channel 主动将数据推送到 Selector。
IO管道:非阻塞 vs 阻塞
非阻塞 与 阻塞IO 的最大区别在于对 channel/stream 的读写方式的不同。
在典型的读取stream数据场景中,可认为有一个 Messager Reader 将stream中的数据分块读出。
在阻塞式IO中,可以从一个 InputStream 读取数据;如果 InputStream 中的数据尚未就绪,这个操作将一直阻塞,直到有数据可读。这导致 Messager Reader 是阻塞式的。
阻塞式的 stream 简化了 Message Reader 的实现。因为无需处理stream中无数据就绪,或只有部分数据就绪的情况。
阻塞式写入stream的操作也类似,无需处理只有部分数据写入stream的情况。
阻塞式IO管道的缺点
虽然阻塞式 Message Reader 更容易实现,但它需要为每个stream创建一个线程来处理数据。
因为stream在数据就绪前会一直阻塞。这导致单个线程无法在 “尝试读取某个stream,但stream数据未就绪” 的情况下转而去读取另一个stream。因为一个线程一旦尝试去stream读数据,它将被阻塞,直到stream中数据就绪。
对于一批来自客户端的并发连接,服务端必须为吗,每个连接创建一个线程进行处理。如果在任一时刻,并发连接数只有几百,也不会有什么问题。但如果是上百万的并发连接,这种阻塞式的设计就无法良好运行。每个线程的线程栈会消耗320KB(32位JVM)或1024KB(64位JVM)的内存。1百万个线程消耗将近1TB的内存!这还没算上服务器用于处理数据的内存。
为了降低线程数量,许多服务端都使用了 线程池 的设计。
来自客户端的连接被维护在一个队列中。一旦线程池中有空闲线程,就从队列中取一个连接,由该线程处理。简化结构如下: