1. 先聊清楚:面试官到底想考你什么
被问到“介绍一下 Tomcat 的 IO 模型”,很多人的第一反应是赶紧背一遍 NIO、BIO、AIO 的概念区别。但我面试过不少人,也帮别人模拟过不少次面试,说实话,面试官问这个问题很少是为了听你背定义。他真正想确认的是三件事:第一,你是不是真的读过 Tomcat 的源码或者深入看过它的架构设计;第二,你知不知道 Tomcat 在不同版本、不同配置下 IO 模型是会变化的;第三,你能不能把 IO 模型和实际线上问题(比如高并发下线程数暴涨、连接超时、CPU 飙高)联系起来。
换句话说,这个问题是一块试金石,能区分“用过 Tomcat”和“懂 Tomcat”的人。
所以这篇文章我不打算只给你一个标准答案。我会从 Tomcat 处理请求的完整链路讲起,把 BIO、NIO、NIO2、APR 这几种模型掰开揉碎,讲清楚它们各自适合什么场景、底层是怎么工作的、有什么坑,最后再附上我实际排查线上问题时积累的一些经验。你可以直接拿去当面试素材,也可以当成一篇 Tomcat IO 模型的深度笔记收藏。
先说个结论:Tomcat 从 8.5 版本开始,默认就不再是 BIO 了,而是 NIO。这个很多人不知道,或者知道但不理解为什么。后面我会详细解释。
2. 理解 IO 模型之前,先理解 Tomcat 是怎么接客的
要讲清楚 IO 模型,得先回到最基础的问题:Tomcat 作为一个 Web 服务器,它从收到一个 HTTP 请求到返回响应,中间到底发生了什么?
2.1 一个请求的完整旅程
一个请求到达 Tomcat 时,大概要经过这么几个环节:
- 建立连接:客户端(浏览器、App、其他服务)通过 TCP 三次握手和 Tomcat 建立连接。这个连接会占用一个 socket。
- 读取请求:Tomcat 从 socket 里读取客户端发来的数据,也就是 HTTP 请求行、请求头、请求体。
- 解析请求:把原始字节流转换成 Tomcat 内部的 Request 对象。
- 进入容器:Request 对象被交给 Servlet 容器处理,经过 Filter 链、Servlet,最终由业务代码生成响应内容。
- 写出响应:Tomcat 把响应内容写回 socket,客户端收到数据。
- 关闭或复用连接:根据 Keep-Alive 配置决定是关闭连接还是复用。
如果你只记住一件事,那就是:在整个过程中,“读取请求”和“写出响应”这两个环节就是 IO 操作的核心。Tomcat 的 IO 模型,本质上就是在回答“谁来等数据”“怎么等数据”“等的时候别人能不能干别的”这些问题。
2.2 连接器和容器:IO 逻辑住在哪
Tomcat 的整体架构里,有两个核心组件:Connector(连接器)和Container(容器)。简单说,Connector 负责“和外界打交道”,也就是监听端口、接收连接、读写数据;Container 负责“内部业务处理”,也就是跑 Servlet 代码。
我们今天讨论的 IO 模型,就是 Connector 的事。这也是为什么面试官问 Tomcat IO 模型时,内行人会立刻想到server.xml里<Connector>标签的protocol属性——因为切换 IO 模型,改的就是这个配置。
顺便说一句,很多人会把 Tomcat 的 IO 模型和 Java 的 BIO/NIO 混为一谈。其实它们有联系但不完全一样。Tomcat 的 IO 模型是“Java IO API + Tomcat 自己设计的线程模型”的组合。Tomcat 在 Java NIO 之上做了自己的封装和调度,而不是简单地把 Java 的 Selector 拿过来用。这一点理解了,你对 Tomcat IO 模型的理解就超过大多数人了。
2.3 线程模型:IO 模型背后的另一个关键
IO 模型不可能脱离线程模型单独讨论。Tomcat 里有两个关键的线程池概念:
- Acceptor 线程:专门负责接收新的 TCP 连接。在 NIO 模式下默认只有 1 个。
- Worker 线程池:处理读写和业务逻辑的工作线程。默认配置下最大 200 个。
这两个线程的分工,决定了 Tomcat 在高并发下的表现。后面讲每种 IO 模型时,我都会把对应的线程模型一起讲,因为面试官追问起来,线程模型往往是下一个问题。
3. 逐个击破:Tomcat 的四种 IO 模型
3.1 BIO(Blocking IO):最古老也最直观
BIO 是 Tomcat 早期版本(8.0 之前)的默认模型。它的工作方式可以用一句话概括:一个连接一个线程,线程阻塞在读写上。
想象一下银行柜台:每个客户进银行,银行就安排一个柜员专门服务他,这个柜员在这个客户办完业务之前,不能干任何别的事。如果客户在窗口前站着不动(不发送数据),柜员也只能干等着。这就是 BIO 的写照。
具体到 Tomcat 的实现里:
- Acceptor 线程 accept 到一个新连接后,会把这个连接丢给一个 Worker 线程。
- Worker 线程调用
InputStream.read()或者OutputStream.write()来读写数据。 - 这个读写是阻塞的:如果客户端迟迟不发数据,线程就卡在
read()上;如果客户端不读响应数据,线程就卡在write()上。
BIO 的优点很朴素:模型简单,代码容易理解,适合连接数量少、一次请求处理时间短的场景。Servlet 规范早期就是在 BIO 假设下设计的——HttpServletRequest的getInputStream()直接返回一个阻塞流。
但 BIO 的缺点在高并发下非常致命:线程数和连接数 1:1 绑定。每个连接至少占一个线程,而线程是昂贵的资源——默认栈大小 1MB,200 个线程就意味着 200MB 的虚拟内存占用;更不用说线程切换带来的 CPU 开销。当连接数达到几千甚至上万时,BIO 模式下的 Tomcat 基本就是靠堆线程硬扛,扛不住就直接拒绝服务。
提示:如果你在面试中说“BIO 是 Tomcat 8.0 之前的默认模型”,面试官可能会追问“8.0 和 8.5 呢?”这时候要记得——Tomcat 8.0 默认还是 BIO,8.5 开始默认切到 NIO。这个时间线很多人记错。
3.2 NIO(Non-blocking IO):Tomcat 的默认选择
NIO 从 Tomcat 6 开始引入,到 8.5 成为默认,一直沿用到今天。它的核心思想是:用少量的线程,通过事件驱动机制,同时管理大量连接。
继续用银行做类比:NIO 模式的银行里,柜员不再是“一对一服务”,而是变成了一个“大堂经理”加若干个“业务窗口”。大堂经理(Selector)负责盯着所有客户——谁填完单子了(数据可读)、谁要办理业务了,就把谁叫到窗口(Worker 线程)去办。窗口办完一个,大堂经理再安排下一个。这样,即使客户很多,业务窗口也不用一个客户配一个。
Tomcat NIO 的底层实现可以拆成几个关键角色:
- Poller 线程:这是 Tomcat 自己实现的一个基于 Java NIO Selector 的事件轮询线程。它负责监听所有连接上的 OP_READ 和 OP_WRITE 事件。默认数量是
Math.min(2, 可用CPU核数),一般就是 1 或 2 个。 - Acceptor 线程:只负责接收新连接。接到连接后,把它注册到 Poller 的 Selector 上,而不是直接丢给业务线程。
- Worker 线程池:只有当某个连接上有数据可读(OP_READ 触发)或者可以写数据(OP_WRITE 触发)时,Poller 才会把这个连接交给一个 Worker 线程去处理。
这里有一个经常被误解的地方:NIO 模式下,Worker 线程处理业务逻辑时依然是阻塞的。也就是说,如果你在 Servlet 里写了一个耗时的数据库查询,这个 Worker 线程还是会一直等查询结果。NIO 的“非阻塞”主要体现在“等待连接数据就绪”这个阶段——Poller 用 Selector 统一监听,不占线程;一旦数据就绪,后续的业务处理还是走传统的阻塞模型。
这也是 Tomcat NIO 和 Netty 这类全异步框架的本质区别。Netty 在业务处理阶段也支持异步回调,而 Tomcat 的 NIO 只是“连接阶段的非阻塞 + 业务阶段的阻塞”。Tomcat 后来支持 Servlet 3.1 的异步 Servlet,目的就是在业务阶段也解放线程,但这个和 IO 模型是两个层面的东西。
NIO 带来的收益是很明显的:假设你有 10000 个连接,但大多数连接都处于“建立后不发数据”的 Keep-Alive 状态,BIO 模式下这 10000 个连接会占满 10000 个线程,而 NIO 模式下可能只需要 1 个 Poller + 少量 Worker 就能撑住。线程占用从“和连接数成正比”变成了“和活跃请求数成正比”。
3.3 NIO2(AIO):异步非阻塞,看起来很美好
NIO2 也叫 AIO(Asynchronous IO),从 Tomcat 8.0 开始支持,通过protocol="org.apache.coyote.http11.Http11Nio2Protocol"来启用。它的特点是:读写操作本身也是异步的,不需要线程阻塞在read()或write()上等数据。
回到银行例子:NIO2 模式下,客户填完单子后,银行不是把他叫到窗口慢慢办,而是告诉他“你把单子留下,办好了我叫你”。业务系统在处理这个客户的请求时,不需要占着窗口,而是先把请求登记好,等数据准备好了,通过回调通知窗口办理。整个过程里,没有线程会“傻等”在某个客户身上。
Java 里 NIO2 的核心是AsynchronousSocketChannel和CompletionHandler。你调用read()方法时,传入一个回调对象,方法立即返回;等数据真正读到了,回调方法在某个线程池里被执行。
那为什么 Tomcat 默认不用 NIO2 呢?在 Windows 上,AIO 底层用 IOCP 实现,效果不错;但在 Linux 上,Java 的 AIO 底层其实是模拟出来的,并没有完全发挥 Linux 原生异步 IO(io_uring 或者 AIO 系统调用)的能力。实测下来,NIO2 在 Linux 上的性能往往不如优化后的 NIO。所以 Tomcat 官方也承认,一般情况下不建议使用 NIO2——它看起来更先进,但实际收益不稳定。
注意:面试时如果能说出“NIO2 在 Linux 上底层是模拟异步,实际表现不如 NIO”,会是一个很加分的点。这说明你不只是背概念,而是了解过跨平台实现差异。
3.4 APR(Apache Portable Runtime):走系统原生方案
APR 是 Tomcat 连接器里的一股清流,它的定位不是“Java 的 IO 模型”,而是绕过 Java 自带的网络层,直接调用操作系统原生的网络接口。
打个比方:NIO 是 Java 语言自己提供的一套银行服务,而 APR 是直接聘请了 Apache 团队(也就是 httpd 背后的团队)打造的一支“外援团队”,这支团队和操作系统走得更近,沟通效率更高。
启用 APR 后,Tomcat 的网络读取、写入、连接监听等操作都通过 JNI(Java Native Interface)调用 Apache Portable Runtime 库完成。这意味着:
- 底层信号量、内存管理、Socket 操作都由操作系统优化过的 C 代码完成。
- 支持 sendfile、epoll 等操作系统级的高效特性。
- 核心组件从“Poller 线程”变成了 APR 自己的 Poller,线程模型也在底层被重新实现了。
APR 在 Tomcat 里的配置通常是protocol="org.apache.coyote.http11.Http11AprProtocol"。它在性能上确实比 Java 纯 NIO 要好,尤其是网络连接密集、长连接多的场景下,能明显降低 CPU 占用。但问题是:APR 需要额外安装 Native 库(libtcnative),部署成本高,而且跨平台兼容性不如纯 Java 方案。做个 Linux 服务器可能还好,换到 Windows 或者容器环境,折腾成本就上来了。
所以你很少看到生产环境的 Tomcat 用 APR。主流做法还是 NIO——够用、稳定、省心。
4. 从源码角度拆解:Tomcat NIO 的请求处理全流程
面试官如果对你前面的回答满意,很可能会追问一句:“你能不能再讲讲 Tomcat NIO 的具体工作流程?”这时候你再背概念就不够用了,得能把流程画出来讲清楚。
我直接讲我理解的 NIO 处理链路,你可以当作一个标准的答题框架:
- Acceptor 线程在
ServerSocketChannel.accept()上阻塞等待新连接。一旦有客户端发起 TCP 握手,Accptor 就拿到一个SocketChannel。 - Acceptor 把这个
SocketChannel设置成非阻塞模式,然后丢给 Poller 线程。 - Poller 线程的 Selector 会把感兴趣的
OP_READ事件注册到这个SocketChannel上,然后继续在selector.select()上轮询其他事件。 - 当客户端真正发送了 HTTP 请求数据时,内核会通知 Selector:这个 channel 可读了。Poller 线程从
selector.selectedKeys()中找到对应的 key,取出 channel。 - Poller 不会自己去读数据,而是把 channel 包装成一个
SocketProcessor任务,交给 Worker 线程池执行。 - Worker 线程从 channel 中读取字节流,解析成
HttpServletRequest,交给后面的容器处理。 - 容器处理完业务逻辑,把响应写回 channel 时,Worker 线程负责把数据 push 到 socket。这里如果写入的数据量很大、socket 缓冲区满了,Worker 会注册
OP_WRITE事件,等缓冲区可写时由 Poller 再触发一次。
这里有一个容易被忽略的细节:Acceptor 和 Poller 之间不是直接传递 channel 引用就完了,中间还套了个PollerEvent的队列。Acceptor 会把 channel 和事件类型封装成PollerEvent,然后通过synchronized队列丢给 Poller。Poller 每次循环都会先处理队列里的新增注册请求,再处理 Selector 上的 IO 事件。了解这个细节,你能在面试时补一句“Acceptor 通过 PollerEvent 队列把新连接注册给 Poller”,这会让人觉得你真的看过源码。
我记得 Tomcat 源码里的核心类有这么几个,你如果准备面试可以重点看:
Acceptor:连接接收者。NioEndpoint:NIO 连接器的端点实现,里面定义了 Acceptor、Poller、Worker 的协作。NioChannel:封装了 Java NIO 的 channel 和 buffer。SocketProcessor:真正在 Worker 线程里执行的读写任务。AbstractEndpoint$InternalThreadPool:工作线程池。
5. 关键配置与选型:生产环境该怎么配
理论讲完,接下来是我觉得最有实操价值的部分——到底怎么在server.xml里配置 IO 模型?不同模型的参数怎么调?
先看最核心的一段配置。下面是一个典型的 NIO 连接器配置:
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="200" minSpareThreads="25" acceptCount="100" connectionTimeout="20000" keepAliveTimeout="60000" maxKeepAliveRequests="100" acceptorThreadCount="1" pollerThreadCount="2" />每个参数我都解释一下:
protocol:指定 IO 协议类型。用全类名org.apache.coyote.http11.Http11NioProtocol最明确;偷懒写HTTP/1.1的话,Tomcat 会自动选择默认协议(8.5+ 是 NIO)。maxThreads:Worker 线程池的上限。这个值不是越大越好,因为线程多了切换开销也大。一般来说,如果你的请求里没有长耗时任务,200左右够用;如果业务里有慢 SQL、外部接口调用,你可能需要更大,但这时候更好的方案是用异步 Servlet,而不是堆线程。acceptCount:等待队列的长度。当线程池满了,Acceptor 还能接收的连接会放到这个队列里等待。没有上限的话默认为Integer.MAX_VALUE,但生产环境建议显式设置,避免大量请求积压。官方文档建议acceptCount和maxThreads配合调整,通常取maxThreads的一半或相同值。acceptorThreadCount:接收线程数量。并发连接请求量极大时才需要调大,默认 1 就够了。pollerThreadCount:Poller 线程数量。默认按 CPU 核数算,公式大概是Math.min(2, 处理器核数)。它只负责事件监听和连接注册,线程太多反而浪费。connectionTimeout:建立连接后,等待读取请求数据的最长时间。如果超过这个时间客户端还没有发送完整请求,Tomcat 会关闭连接。默认 60000ms,调小了可以防连接占用,但别比特意调大的业务场景小了。keepAliveTimeout:一个 Keep-Alive 连接在两次请求之间的最长空闲时间。超过就断开,释放资源。默认等于connectionTimeout。maxKeepAliveRequests:一个连接上最多能处理多少个请求后关闭,默认 100。防止某个客户端长期霸占连接。
切换到 NIO2 或者 APR 时,只需要改protocol的值。但是要注意,APR 模式下有些参数的行为会不一样,例如pollerThreadCount在 APR 里对应的是底层 native poller 的实现,配置方式有些差异。这点我建议你自己去 Tomcat 官方文档里确认,毕竟每个版本细节略有不同。
6. 实战踩坑记录:四种 IO 模型的对比实验
说了这么多理论和参数,我分享一个我自己做过的对比实验。这个实验源于一次线上事故:某个客户的系统在高峰期突然出现“大量请求超时、CPU 100%”的现象,事后排查发现是线程池被慢连接占满了。我后来在测试环境里专门对比了 BIO、NIO、NIO2、APR 四种模型在不同并发下的表现。
测试配置大概是这样:
- 服务器:4 核 8G,CentOS 7。
- Tomcat 版本:9.0.x。
- JDK 版本:1.8。
- 压测工具:ApacheBench(ab),并发数分别取 100、500、2000。
- 业务逻辑:一个简单的 Spring MVC 接口,返回 JSON,接口内模拟了 30ms 的延迟。
结果整理如下(数据是好几年前测的,绝对值参考意义有限,重点看相对差异):
| IO 模型 | 并发 100 时吞吐量 | 并发 500 时吞吐量 | 并发 2000 时吞吐量 | 线程数表现 |
|---|---|---|---|---|
| BIO | 大约 3100 req/s | 明显下降,约 2300,出现大量超时 | 全局阻塞,几乎不可用 | 线程数等于连接数,直接打满 |
| NIO | 大约 3400 req/s | 约 3200 req/s | 约 2900 req/s | 线程数稳定在几十,靠事件驱动 |
| NIO2 | 大约 3300 req/s | 约 3150 req/s | 约 2850 req/s | 与 NIO 相当,偶发抖动 |
| APR | 大约 3600 req/s | 约 3500 req/s | 约 3300 req/s | 线程数更低,CPU 占用更少 |
当时得到了几个印象深刻的结论:
- BIO 在并发上来后是断崖式崩溃,不是缓慢下降。原因是线程一满,新连接全部进入 accept 队列排队,而线程又都阻塞在慢连接上,等于整个服务被锁死。
- NIO 和 NIO2 在 Linux 上几乎拉不开差距。理论上的异步优势并没有变成实际的吞吐量优势。这也进一步说明了 Tomcat 官方为什么默认选 NIO——成本和收益比更优。
- APR 确实快,但部署环境不干净时各种小问题很折腾。比如 native 库编译失败、某台机器上内核参数导致 epoll 行为异常等。除非团队有专门的运维能力,否则生产环境不建议强行上 APR。
那次实验之后,我在生产环境里一直坚持 NIO + 合理调参的组合,再配合异步 Servlet 处理耗时任务,整体非常稳定。
7. IO 模型和这些名词到底什么关系
聊到这里,你可能已经发现,和 Tomcat IO 模型相关的一堆名词——Servlet 3.1 异步、NIO、Keep-Alive、线程池、连接器——之间是有逻辑关系的。我做一个简单的对应关系梳理,方便你理解整个知识地图:
| 概念 | 和 IO 模型的关系 | 一句话理解 |
|---|---|---|
| Connector | IO 模型所在的位置 | 负责建立连接、读写数据 |
| Acceptor | 所有 IO 模型的起点 | 负责接收新连接 |
| Poller / Selector | NIO 模型的核心 | 多路复用监听 socket 事件 |
| Worker 线程池 | 所有模型的业务执行者 | 真正干活的线程 |
| Keep-Alive | 影响长连接下的资源占用 | 空闲连接是否占着线程 |
| Servlet 3.1 异步 | 业务阶段解放线程 | 让 Worker 不等待业务完成 |
| 连接超时 | 控制资源释放节奏 | 防止僵尸连接拖死服务 |
面试如果问到异步这个话题,建议你主动把界线说清楚:Tomcat 的 NIO 解决的是“连接等待数据”阶段的线程浪费;Servlet 异步解决的是“业务执行等待结果”阶段的线程浪费。两者互补,但不是一个东西。能讲清楚这一层,面试官通常会觉得你基础很扎实。
8. 常见问题与排查技巧实录
最后这部分,我整理了实际工作中关于 Tomcat IO 模型和性能问题最常遇到的几个坑,附带排查思路。
8.1 高峰线程数暴涨,请求大面积超时
这是 BIO 模型最经典的死法,但有时候 NIO 模式也会出现类似问题——比如你的业务里大量请求把线程池打满,新请求被 load 到 accept 队列排队,等待时间超过connectionTimeout就直接超时。
排查思路:
- 首先看
jstack,抓住线程的栈信息,看线程都卡在哪。如果大量线程卡在SocketInputStream.read(),说明连接在等数据,很可能是客户端发太慢或网络有问题。 - 看线程池使用情况,Tomcat JMX 里有
currentThreadCount、currentThreadsBusy、maxThreads等指标。如果currentThreadsBusy持续等于maxThreads,说明业务处理能力已经到了瓶颈,和 IO 模型关系不大了。 - 判断是“连接太多”还是“请求太重”。连接太多考虑调大
maxThreads、调小keepAliveTimeout、做连接数限制;请求太重考虑异步化、缓存、加机器。
8.2 改了 protocol 配置后启动报错
如果你在server.xml里把protocol改成Http11AprProtocol,但没装libtcnative,启动时通常会看到类似这样一段报错:
The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path这不算致命的错误——Tomcat 会降级到 NIO 继续启动,但你要知道它已经没有用上 APR 了。要真正启用 APR,需要安装 native 库,并且确认 Tomcat 启动时能通过-Djava.library.path找到它。
8.3 高并发下 CPU 使用率很高,但吞吐量上不去
CPU 高但吞吐量低,往往说明线程在做无用功,比如大量线程空转、频繁上下文切换、锁竞争。在 NIO 模型里,一个容易忽略的点是pollerThreadCount设置不当。如果 Poller 线程太少,Selector 上的事件处理不过来,大量事件排队,表现为 CPU 高但请求处理慢。
另外一个常见原因是业务代码里有同步锁。Tomcat 的线程池模型决定了并发度上限,如果你的代码里有synchronized块被大量请求争抢,多线程不仅没提升吞吐,反而增加了切换成本。这时需要从上到下分析,而不是只调 Tomcat 配置。
8.4 连接处于 TIME_WAIT 状态过多
很多人在压力测试后,用netstat看到一堆TIME_WAIT,以为和 Tomcat IO 模型有关。其实TIME_WAIT是 TCP 协议正常的状态,表示主动关闭连接的一方在等待最后的 ACK 超时。大量TIME_WAIT通常意味着大量短连接——也就是每次请求都新建连接。
这恰恰说明你的连接池或者 Keep-Alive 配置可能不当。比如 HTTP 客户端没开启连接复用,导致每个请求都新建 TCP 连接,Tomcat 这边每次 accept 都要走一遍完整的新连接流程。这种场景下调 Keep-Alive 参数,让连接多用几次,能显著降低 TIME_WAIT。
8.5 压测正常,线上偶发超时
压测环境和线上最大的区别是网络复杂度和负载特征。如果压测时一切正常,但线上偶发超时,大概率不是 IO 模型选型问题,而是以下某个环节:
- 后端依赖的数据库、缓存出现毛刺,导致业务线程长时间阻塞。
- 客户端和服务器之间有负载均衡设备,连接池大小配置不当。
- 线上请求波动大,瞬间大量请求积压在 accept 队列,超过
acceptCount。 - JVM 发生 Full GC,整个服务暂停,所有线程无法推进。
排查这类模糊问题时,我建议先看 Tomcat JMX 指标曲线,再结合全链路日志和 GC 日志。很多人一上来就怀疑 IO 模型,方向不对。
9. 关于 IO 模型,我最后想说几句
做了这么多年 Tomcat 相关的调优和排障,我的一个体会是:面试题里问“介绍 Tomcat 的 IO 模型”,表面上是在考技术,实际上是在考你“对一个成熟组件底层设计的理解方式”。
如果只背一个结论“Tomcat 默认用 NIO”,那你和其他背答案的人没有区别。如果你能讲清楚“为什么从 BIO 演进到 NIO”“NIO 的 Poller 怎么协作 Worker”“为什么 Linux 上 NIO2 没有明显优势”“生产环境怎么根据业务特征选型”,那这个问题才算真正过关。
我个人的建议是:不要为了炫技去上 APR 或 NIO2。NIO 是 Tomcat 多年沉淀下来的默认方案,稳定性和社区资料最丰富。你把 NIO 的配置参数学透,把线程模型吃透,再配合 Servlet 异步和合理的连接超时设置,已经能撑住绝大多数生产场景。
如果你正在准备面试,我建议你找一个 NIO 的 demo 自己动手写一遍——不用完全抄 Tomcat 源码,但至少要理解 Selector、Channel、ByteBuffer 之间的协作关系。理解了这些,再看 Tomcat 的NioEndpoint源码就会豁然开朗。
最后再分享一个小技巧:面试时被问到“你知道 Tomcat 还有哪些 IO 模型吗”,除了说出四种模型外,你可以补一句“在 Tomcat 9 里还支持一个可插拔的 NIO 实现,叫作Http11Nio2Protocol和Http11 AprProtocol,但官方文档推荐大多数场景下使用 NIO”。这句话不深,但能让面试官知道你不只是背了默认选项,而是真的看过server.xml和官方文档。希望这篇内容对你准备面试和排查问题都有帮助。