为何使用NIO和Selector的应用线程数低于Thread per request模型?相关疑问
Java NIO 非阻塞IO理解与疑问解答
你的理解正误梳理
Thread per request模型
你的理解完全正确:
- 服务器每接受一个新连接就创建一个独立线程,持有对应客户端套接字的引用。
- 每个线程都是阻塞模式,必须处理完当前读取的消息后,才能继续等待下一次IO事件。
非阻塞IO流程
整体流程的理解是对的,但有一处细节需要修正:
NIO的Channel本身就是非阻塞模式,不是在读取消息后“解除阻塞”。Event Loop线程读取就绪的Channel数据后,不需要等待该Channel的后续操作完成,就能立刻去处理其他就绪的Channel(比如其他客户端的读事件、新连接事件)——这才是非阻塞IO的核心优势之一。
其余流程描述准确:
- 客户端Channel初始向Selector注册读事件
- Event Loop读取数据后,将业务逻辑提交给线程池处理,自身继续处理其他就绪事件
- 业务线程处理完成后,通知Selector唤醒Event Loop,将该Channel的关注事件改为写操作
- 写入响应后,再把Channel的关注事件改回读操作
你的疑问解答
1. 依赖线程池处理业务,会不会和普通IO一样创建大量线程?为什么NIO更好?
不会。Thread per request模型是每个连接(甚至每个请求)都创建新线程,线程的创建、销毁和上下文切换开销极大,当连接数达到数千甚至上万时,系统会因为线程过多导致CPU频繁切换上下文,性能急剧下降。
而NIO的线程池是线程复用的:线程池会维护固定数量(或动态调整但有上限)的线程,处理完一个业务任务后,线程会回到池中等待下一个任务,不会随消息数量无限创建。同时,Event Loop线程的数量通常和CPU核心数相当(比如Netty默认是CPU核心数*2),用来处理所有IO事件,不需要为每个连接分配线程。
NIO的优势总结:
- 用少量Event Loop线程处理大量连接的IO事件,避免了线程爆炸
- 线程池复用线程,降低线程创建销毁的开销
- 非阻塞IO让Event Loop线程不会被单个连接的IO操作阻塞,资源利用率更高
2. 线程池数量有限时,Event Loop提交任务会不会被阻塞?
这取决于线程池的拒绝策略和任务队列配置:
- 如果使用默认的
AbortPolicy,当线程池满且任务队列也满时,会直接抛出RejectedExecutionException,但这种情况可以通过合理配置避免。 - 实际生产中常用
CallerRunsPolicy:当线程池无法接受任务时,会让提交任务的线程(也就是Event Loop线程)直接执行该任务。这样Event Loop会暂时处理业务逻辑,不会被阻塞,只是会暂时无法处理其他IO事件,避免任务丢失。 - 另外,也可以配置有界的任务队列(比如
ArrayBlockingQueue)或无界队列(比如LinkedBlockingQueue)来缓冲任务,只要队列未满,Event Loop提交任务就不会阻塞。
当然,合理设置线程池的核心线程数、最大线程数和队列大小,是避免这个问题的关键——通常根据业务的CPU密集型或IO密集型来调整:CPU密集型任务的线程数建议设置为CPU核心数+1;IO密集型任务的线程数可以设置为CPU核心数*2或更高。
内容的提问来源于stack exchange,提问作者King
相关产品推荐
相关产品推荐

