Tomcat 支持哪些 IO 模型(连接器)?BIO、NIO、NIO2 和 APR 有什么区别?
简化版
Tomcat 用 Connector(连接器) 接收网络连接、处理 IO,它支持多种 IO 模型(由 ProtocolHandler 决定):① BIO(同步阻塞,一连接一线程,性能差,Tomcat 8.5 起已移除);② NIO(同步非阻塞,基于 Java NIO 的多路复用,用少量线程管大量连接,现在的默认);③ NIO2(AIO)(异步 IO,基于 Java AIO,Linux 上底层仍是 epoll 模拟、优势不明显,用得少);④ APR(Apache Portable Runtime,基于本地 C 库直接调操作系统的 epoll/sendfile,性能最高,但要单独装本地库)。核心演进:BIO 因性能差被淘汰 → NIO 成为默认(多路复用扛高并发)→ APR 追求极致性能(本地库)。一句话:现在 Tomcat 默认用 NIO,配合它的 Acceptor/Poller/Worker 线程模型扛高并发。
详细版
Tomcat 的四种 IO 模型(Connector 类型):
| 模型 | 底层 | 特点 | 现状 |
|---|---|---|---|
| BIO | 传统阻塞 IO | 一连接一线程,并发差 | Tomcat 8.5 起移除 |
| NIO | Java NIO(多路复用) | 少量线程管大量连接 | 默认 |
| NIO2 | Java AIO(异步) | 异步回调,但 Linux 上是 epoll 模拟 | 可选,用得少 |
| APR | Apache 本地 C 库 | 直接用 OS 的 epoll/sendfile,性能最高 | 需装本地库,追求极致性能时用 |
Connector 的组成(Coyote 组件):
Connector 内部核心:ProtocolHandler
ProtocolHandler = Endpoint(处理网络连接和 IO)+ Processor(解析协议,如 HTTP)
不同 IO 模型对应不同的 ProtocolHandler 实现:
NIO:Http11NioProtocol(默认)
NIO2:Http11Nio2Protocol
APR:Http11AprProtocol
配置(server.xml):
<Connector protocol="org.apache.coyote.http11.Http11NioProtocol" .../>
或简写 protocol="HTTP/1.1"(Tomcat 自动选择,一般是 NIO)
为什么 BIO 被淘汰:
BIO:一个连接一个线程,线程从"接受连接"到"处理完请求"全程占用
→ 1 万并发连接 = 1 万个线程 → 内存爆炸、上下文切换开销大
→ 而且很多连接在"等数据"(keep-alive 长连接空闲时),线程白白占着
NIO:用多路复用(Selector/epoll)监听大量连接,少量线程处理就绪的
→ 几十个线程管几万连接 → 高并发下完胜 BIO
→ 所以 Tomcat 8.5 直接移除了 BIO,默认 NIO
⚠️ Tomcat 8.5+ 已经移除了 BIO 连接器——所以现在讨论「Tomcat 的 IO 模型」实际就是 NIO(默认)、NIO2、APR 三选一,BIO 只是历史。而且 NIO 是绝对主流的默认选择——它用「Acceptor 接连接、Poller 用 Selector 轮询就绪、Worker 处理业务」的线程模型(就是 Reactor 模式),在绝大多数场景下性能足够好。APR 虽然更快,但要额外安装本地库(
libtcnative)、增加运维复杂度,只在对性能极度敏感时才用。
完整版教学
一、Connector 的职责:连接与 IO 的入口
Tomcat 的架构分「Connector(连接器)+ Container(容器)」——Connector 负责「网络层」(接受连接、读写字节、解析协议),Container 负责「Servlet 层」(把请求交给对应 Servlet 处理)。IO 模型的选择就发生在 Connector 这一层:
一个 HTTP 请求进来:
Connector(IO 模型在这里):
- 接受 TCP 连接
- 用某种 IO 模型(BIO/NIO/APR)读取字节
- 把字节解析成 HttpServletRequest 对象
→ 交给 Container(Engine→Host→Context→Wrapper)→ 最终到 Servlet
Connector 的核心组件叫 Coyote,它内部的 ProtocolHandler 决定了「用哪种 IO 模型 + 处理什么协议」。IO 模型的差异,本质是「Connector 怎么读写网络数据」的差异——是阻塞地一连接一线程读(BIO),还是用多路复用少线程管多连接(NIO),还是用本地库直接调 OS(APR)。理解「IO 模型是 Connector 层的事、决定了 Tomcat 怎么处理网络连接」,就抓住了这道题的定位——它讲的是 Tomcat 的「网络处理能力」。
二、BIO:为什么被淘汰
BIO(Blocking IO,同步阻塞)是最早的模型,也是最简单的——一个连接分配一个线程,从头管到尾:
BIO 模型:
每接受一个连接 → 分配一个线程处理它的整个生命周期
这个线程要:读请求(阻塞等数据)→ 处理 → 写响应(阻塞等写完)→ 处理下一个请求(keep-alive)
问题:
① 线程数 = 连接数:1 万连接 = 1 万线程 → 内存爆炸(每线程约 1MB 栈)+ 上下文切换开销
② 线程大量空等:HTTP keep-alive 下连接空闲时,线程还占着(等下一个请求)却没事干
③ 无法扛高并发:线程是有限资源,连接一多就耗尽
BIO 的致命缺陷是「线程和连接一比一绑定」——在「连接多但大部分空闲」的 Web 场景(尤其 keep-alive 长连接)下,大量线程被空闲连接占着,浪费严重、扛不住高并发。这就是经典的 C10K 问题(一万连接就崩)。所以 Tomcat 8.5 直接移除了 BIO 连接器,它只剩历史意义。理解「BIO 因『一连接一线程』扛不住高并发而被淘汰」,就理解了为什么要有 NIO。
三、NIO:默认的多路复用模型
NIO(Non-blocking IO,同步非阻塞)是 Tomcat 现在的默认模型,核心是用 IO 多路复用(Selector/epoll)让少量线程管理大量连接:
NIO 模型(就是 Reactor 模式):
不再一连接一线程,而是用少量线程 + Selector 监听所有连接的事件
Tomcat NIO 的三类线程:
Acceptor:接受新连接(少量,通常 1 个)
Poller:用 Selector 轮询,找出"就绪"(有数据可读)的连接
Worker(线程池):处理就绪连接的业务(真正干活)
关键:连接空闲时不占线程(在 Selector 上"挂着")
只有"就绪"的连接才分配 Worker 处理 → 少量线程管几万连接
NIO 相比 BIO 的突破是「解耦了连接数和线程数」——连接空闲时不占线程(挂在 Selector 上),只有有数据的连接才分配线程处理。所以几十个线程能管几万个连接,高并发下完胜 BIO。这本质就是 Reactor 模式(多路复用 + 事件驱动),和 Netty 的线程模型同源。Tomcat 的这三类线程(Acceptor/Poller/Worker)配合 maxThreads、maxConnections、acceptCount 等参数决定并发能力。NIO 是绝大多数场景的最优选择——性能足够好、无需额外依赖,所以是默认。理解「NIO = 多路复用 + Reactor + 少线程管多连接」,就理解了它为什么取代 BIO 成为默认。
四、NIO2(AIO):异步但优势不明显
NIO2 基于 Java AIO(异步 IO),理论上更先进——它是「完成通知」而非「就绪通知」(内核帮你读写完成后回调,而不是通知你「可以读了」你自己读):
NIO(同步非阻塞):Selector 通知"可以读了" → 你自己读(读的动作占线程)
NIO2/AIO(异步):发起读后不管 → 内核读完了回调通知你(读的动作内核做)
看起来 NIO2 更好(连读的动作都省了),但实际:
在 Linux 上,Java AIO 底层是用 epoll 模拟的(Linux 没有成熟的原生异步 IO)
→ 并没有用真正的内核异步能力,性能不比 NIO 好、还多一层封装
→ 只有 Windows 的 IOCP 才是真异步,但服务器多用 Linux
所以 NIO2 在 Tomcat 里存在但用得少——它的理论优势(真异步)在 Linux 服务器上兑现不了(底层还是 epoll 模拟),性能和 NIO 差不多甚至更差。这和「为什么 Netty 不用 AIO 而用 NIO」是同一个原因:Linux 缺乏成熟的异步 IO,AIO 是假异步。所以实践中 Tomcat 极少用 NIO2,默认还是 NIO。理解「NIO2 是 AIO、理论先进但 Linux 上无优势」,就知道它为什么不是主流。
五、APR:追求极致性能的本地库
APR(Apache Portable Runtime)是性能最高的模型,但它不用 Java 的 IO,而是基于本地 C 库直接调用操作系统的底层能力:
APR 模型:
用 Apache 的本地库(libtcnative,C 写的)
直接调用操作系统的 epoll、sendfile(零拷贝)等系统调用
→ 绕过 JVM 的一些开销,直接用 OS 最高效的机制
优势:
性能最高(本地库 + 直接用 OS 的 epoll/sendfile 零拷贝)
尤其在处理静态资源、SSL 时优势明显(本地库处理更快)
代价:
① 要单独安装本地库(libtcnative + OpenSSL),和操作系统绑定
② 增加部署/运维复杂度(本地库版本、平台兼容)
③ 出问题更难排查(C 库层面)
APR 是「用本地库换极致性能」——它绕过 JVM 直接用 OS 的 epoll、sendfile(零拷贝),比纯 Java 的 NIO 更快,尤其在静态资源和 HTTPS 场景。但代价是要装本地库、增加运维复杂度、平台绑定。所以 APR 只在「对性能极度敏感、且能接受额外运维成本」时用(如高流量的静态资源服务、SSL 密集场景)。绝大多数应用用 NIO 就够了,不必上 APR。理解「APR = 本地库 + 极致性能但有运维代价」,就知道它是「特殊场景的性能优化」,不是默认。
六、选型与演进总结
Tomcat IO 模型的演进和选型:
演进脉络:
BIO(一连接一线程,扛不住高并发)
↓ 被淘汰(Tomcat 8.5 移除)
NIO(多路复用,少线程管多连接,Reactor 模式)← 现在的默认
↓ 追求极致性能
APR(本地库直调 OS epoll/sendfile,最快但要装本地库)
选型建议:
| 场景 | 选择 |
|---|---|
| 绝大多数应用 | NIO(默认,性能足够、无额外依赖) |
| 对性能极度敏感、静态资源/SSL 密集 | APR(本地库,最快,但要装 libtcnative) |
| 追新的异步模型 | NIO2(但 Linux 上无优势,不推荐) |
| 老旧需求 |
核心结论:Tomcat 现在默认 NIO,绝大多数场景 NIO 就是最优解——它用 Reactor 模式(多路复用 + Acceptor/Poller/Worker 线程)扛高并发,性能足够好且无需额外依赖。APR 是「性能极致党」的选择(有运维代价),NIO2 因 Linux 上无优势而少用,BIO 已成历史。面试答这题的关键是讲清「BIO 因一连接一线程被淘汰、NIO 用多路复用成默认、APR 用本地库追极致、NIO2 因假异步少用」这条演进和选型逻辑。
记忆钩子:「Tomcat Connector 的 IO 模型:BIO(一连接一线程扛不住高并发,8.5 移除)、NIO(多路复用 Reactor,少线程管多连接,默认)、NIO2(AIO 异步但 Linux 上 epoll 模拟无优势,少用)、APR(本地库直调 OS epoll/sendfile 最快但要装 libtcnative);由 ProtocolHandler 决定;默认 NIO 够用、APR 追极致、NIO2 别用」。
七、常见误区与追问
- 误区:Tomcat 现在还能用 BIO 连接器。 Tomcat 8.5 起已移除 BIO——因为「一连接一线程」扛不住高并发;现在是 NIO(默认)、NIO2、APR 三选一。
- 误区:NIO2(AIO)比 NIO 性能好。 在 Linux(服务器主流)上 Java AIO 是用 epoll 模拟的假异步,性能不比 NIO 好、还多一层封装;只有 Windows 的 IOCP 才是真异步。
- 误区:APR 是纯 Java 实现的更优 NIO。 APR 基于本地 C 库(libtcnative),直接调 OS 的 epoll/sendfile,不是 Java IO;性能最高但要单独安装本地库、有运维代价。
- 误区:IO 模型选 APR 就一定更好。 APR 更快但要装本地库、增加运维复杂度、平台绑定;绝大多数应用用 NIO 就够,只有对性能极度敏感(静态资源/SSL 密集)才值得上 APR。
- 追问:Tomcat 的 NIO 模型和 Reactor 有什么关系? Tomcat NIO 就是 Reactor 模式——用 Acceptor 接连接、Poller 用 Selector 轮询就绪连接、Worker 线程池处理业务,多路复用让少线程管大量连接,和 Netty 的线程模型同源。
- 追问:为什么 Tomcat 默认用 NIO 而不是 APR? NIO 是纯 Java、无需额外依赖、性能足够好,适合绝大多数场景;APR 虽更快但要装本地库、增加运维复杂度、平台绑定,只在性能极致场景才值得,所以默认选更通用的 NIO。
- 追问:Connector 里的 ProtocolHandler 是什么? 它决定「用哪种 IO 模型 + 处理什么协议」,由 Endpoint(处理网络连接和 IO)+ Processor(解析协议如 HTTP)组成;NIO 对应 Http11NioProtocol、APR 对应 Http11AprProtocol。
八、加强记忆
Tomcat 用 Connector(连接器,核心是 Coyote 的 ProtocolHandler) 处理网络连接和 IO,IO 模型的选择就在这一层。四种模型的演进和选型:① BIO(同步阻塞,一连接一线程,keep-alive 下大量线程被空闲连接占着、扛不住高并发即 C10K,Tomcat 8.5 起已移除);② NIO(同步非阻塞,基于 IO 多路复用(Selector/epoll)+ Reactor 模式——Acceptor 接连接、Poller 轮询就绪、Worker 处理,少线程管大量连接、解耦了连接数和线程数,是现在的默认);③ NIO2(AIO)(异步,「完成通知」而非「就绪通知」,但 Linux 上是 epoll 模拟的假异步、无优势,少用,同 Netty 不用 AIO 的原因);④ APR(基于本地 C 库 libtcnative,直接调 OS 的 epoll/sendfile 零拷贝,性能最高尤其静态资源/SSL,但要装本地库、有运维代价、平台绑定)。选型:绝大多数用 NIO(默认、够用、无依赖),性能极致才上 APR,NIO2 因假异步别用,BIO 已成历史。一句话「BIO 一连接一线程被淘汰、NIO 多路复用 Reactor 成默认、NIO2 假异步少用、APR 本地库追极致,默认 NIO 就够」。