背景
IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有 一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO 的非阻塞模式, 使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可 用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以 继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它 完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞 IO 的空闲时间用于在其它通道上 执行 IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。 这里的这个线程指的是什么线程
简单来说,这里的“线程”指的是执行 Java 代码的系统线程(Worker Thread),但在 NIO 的语境下,它通常特指 Selector(选择器)所在的那个轮询线程。
为了让你透彻理解,我们对比一下 BIO(传统 IO)和 NIO 中线程角色的变化:
1. 在 BIO(阻塞 IO)中:它是“专职保姆”
在传统 IO 里,这个线程通常是 Connection Thread(连接线程)。
-
行为:每来一个 Socket 连接,服务器就得开一个新线程。
-
状态:当它调用
read()时,如果客户端没发数据,这个线程就**“死等”**在原地。 -
代价:如果有一万个冷启动的连接,你就得开一万个线程。绝大多数线程都在阻塞睡觉,极大地浪费了系统内存和上下文切换的 CPU。
2. 在 NIO(非阻塞 IO)中:它是“多面手调度员”
在 NIO 里,这个线程通常被称为 Selector Thread(选择器线程/反应堆线程)。
它的工作模式:
这个线程不再盯着一个死胡同看,而是手里拿着一个名册(Selector),名册上登记了成千上万个通道(Channel)。
-
非阻塞 read():线程问 Channel A:“有数据吗?”。如果没有,
read()立即返回 0 或 -1,线程绝不逗留,转头就去问 Channel B 或者去处理别逻辑。 -
多路复用:线程调用
selector.select()。这个方法会询问内核:我管辖的这一千个 Channel 里,谁的数据准备好了?内核告诉它:“C 和 D 准备好了”。 -
高效利用:于是这个线程迅速跳过去把 C 和 D 的数据读出来处理掉。
3. 这里的“线程”到底干了什么?
你提到的“该线程可以继续做其他的事情”,具体包括:
-
处理其他 Channel:处理名册上其他已经就绪的 IO 请求。
-
业务逻辑计算:在等待 IO 的空闲时间里,计算一些复杂的业务公式。
-
任务分发:把读到的数据包装成一个 Task,丢给后端的线程池(ThreadPool)去处理,而它自己继续回来监听新的 IO 事件。
总结:这就是“单线程管理多 IO”
在 Netty 等高性能框架中,这种线程被称为 EventLoop(事件循环)。
-
BIO 里的线程:是“排队窗口的办事员”,办不完你的业务,后面的人都得等着,他自己也动不了。
-
NIO 里的线程:是“餐厅的服务员”,点完菜把单子往厨房一扔(发起请求),立马就去给下一桌倒茶了(处理其他通道),等菜好了(数据准备就绪)他再过来端菜。
这就是为什么 Redis、Node.js 或者 Netty 能够用极少数的线程,撑起成千上万并发连接的核心秘密。
你要不要了解一下在 Java 代码里,如何用 Selector 实际写一个简单的非阻塞 Server 示例?我们可以拆解一下它的核心 API。