为什么 Tomcat 8.x 以后默认使用 NIO 而不是 BIO?
Tomcat 从 8.x 版本开始将默认的 I/O 模型从 BIO (Blocking I/O) 切换为 NIO (Non-blocking I/O),并在 8.5 版本中彻底移除了 BIO 支持。这一变革主要基于以下核心原因:高并发需求、资源利用效率、以及应对现代网络环境(如慢速客户端和长连接)的挑战。
以下是详细的深度解析:
1. 突破并发瓶颈 (The C10K Problem)
BIO (阻塞式 I/O) 的局限性:
BIO 采用的是 “一个连接一个线程” (One-thread-per-connection) 的模型。每当有一个客户端连接进来,Tomcat 就必须分配一个独立的线程来处理。- 如果并发量只有几百,BIO 没问题。
- 一旦并发量达到上千甚至上万(C10K 问题),服务器就需要创建成千上万个线程。
- 后果: 线程是操作系统昂贵的资源。过多的线程会导致内存溢出(OOM),且 CPU 会花费大量时间在“线程上下文切换”上,而不是处理实际业务,导致系统崩溃。
NIO (非阻塞 I/O) 的优势:
NIO 采用 “多路复用” (I/O Multiplexing) 模型。它引入了Selector(选择器)机制,允许 一个(或少量)线程管理成千上万个连接。- 只有当连接真正有数据可读或可写时,才会分配工作线程去处理。
- 结果: Tomcat 可以用极少的线程(例如几百个)支撑上万的并发连接,极大地提升了并发处理能力。
2. 解决“慢客户端”和网络延迟问题
这是 BIO 被抛弃的一个非常关键的实际场景原因。
- BIO 场景下的慢客户端:
假设用户使用 2G/3G 网络或者信号很差,上传一个文件需要 1 分钟。在 BIO 模式下,处理该请求的线程必须全程阻塞等待,直到数据接收完毕。在这 1 分钟内,这个线程什么也干不了,完全被浪费了。 - NIO 场景下的慢客户端:
NIO 将连接注册到Poller中。如果客户端发送数据很慢,Tomcat 的工作线程不会阻塞等待。它会去处理其他请求,等到数据终于传输完毕了,Selector 会通知工作线程:“嘿,数据好了,来处理吧”。- 这使得服务器不再受制于客户端的网速,极大提升了吞吐量。
3. 对 HTTP Keep-Alive (长连接) 的优化
现代 Web 应用(HTTP/1.1 及 HTTP/2)普遍使用 Keep-Alive 长连接来复用 TCP 通道。
- BIO 的浪费: 在 Keep-Alive 模式下,即使客户端没有发送新请求,连接依然保持打开。在 BIO 中,这意味着必须有一个线程一直守着这个空闲连接,导致大量线程处于“占着茅坑不拉屎”的状态。
- NIO 的高效: NIO 可以将这些空闲的 Keep-Alive 连接交给
Poller托管,不占用任何工作线程。只有当客户端发起新的 HTTP 请求时,才会唤醒线程处理。这使得 Tomcat 可以轻松维持海量的长连接。
4. 资源消耗与上下文切换
- 内存: 每个线程都有独立的栈空间(通常默认为 1MB 左右)。BIO 模式下高并发意味着巨大的内存消耗。NIO 显著减少了线程数,从而节省了内存。
- CPU: 线程越多,操作系统在线程间切换(Context Switch)的开销就越大。当线程数超过 CPU 核心数一定比例后,性能会急剧下降。NIO 维持较少的线程数,使 CPU 能更专注于业务计算。
5. Java NIO 技术的成熟
在 Tomcat 早期版本(如 5.x, 6.x),Java 的 NIO (New I/O) 库(始于 JDK 1.4)还不够成熟,存在一些 Bug(如著名的 Linux Epoll 空轮询 Bug),且编写复杂度极高。
到了 Tomcat 8 的时代(基于 JDK 7/8),Java NIO 已经非常成熟稳定。Tomcat 官方团队重写了连接器(Connector),使得 Java 原生 NIO 的性能已经可以媲美甚至超越依赖本地库的 APR (Apache Portable Runtime) 模式,且具备更好的跨平台性(纯 Java 实现,无需安装本地库)。
总结
Tomcat 8.x 默认使用 NIO 是为了适应现代互联网高并发、长连接、移动网络环境的必然选择。
- BIO: 就像餐厅里一个服务员只服务一桌客人,客人看菜单看半小时,服务员就傻站半小时,效率极低。
- NIO: 就像餐厅里一个服务员看管十桌客人,谁举手点菜就去谁那里,空闲时间服务员可以去忙别的,效率极高。