如果你在简历里写了"手写实现简易版 Tomcat",这绝对是一个能和面试官聊上很久的亮点!
这里为你整理了一套**"满分叙述话术"以及"延伸出的高频面试题库"**,建议按照这个逻辑去准备。
一、 🗣️ 面试话术:如何清晰地口述整个 Tomcat 流程?
面试官: "我看你手写了一个 Tomcat,能给我讲讲它的运行工作流程吗?"
🔥 满分叙述模板(建议背诵):
"面试官您好,我手写的 MyTomcat 核心流程主要分为两大阶段:启动初始化阶段 和 请求处理阶段。
- 首先是启动初始化阶段(类似 Spring 的 IoC 机制):
Tomcat 启动前,会先执行ServletConfigMapping中的一段静态代码块。它利用反射 和文件遍历 技术,扫描我在指定包下的所有类。只要发现类头上带有@WebServlet注解,就会通过反射的newInstance()提取把这个 Servlet 实例化成一个单例对象 ,并以注解里的 URL 路径做 Key、对象做 Value,存进一个全局的HashMap中。这就建立起了一张**'路由映射表'**。 - 其次是服务器监听阶段:
我在主线程中利用ServerSocket绑定了 7788 端口,并开启了一个while(true)的死循环,调用accept()方法阻塞等待客户端连接。 - 最后是请求处理与响应阶段:
当浏览器发来 HTTP 请求时,连接建立。我取出网络连接的InputStream,读取字节流转换成字符串(即 HTTP 请求报文)。接着我对字符串进行切割,解析出请求方式(GET/POST)和请求路径(如 /first) 。
拿到路径后,我去全局的HashMap里找对应的 Servlet 实例,如果找到了,就调用它的service()方法,最终分发到doGet()或doPost()里处理业务代码。业务处理完后,再把拼接好的 HTML 字符串通过OutputStream按照 HTTP 协议格式写回给浏览器,最后关闭Socket。这就是一整个完整闭环。"
二、 💼 连环炮:由这个项目引发的核心面试题(深度提问)
当你说完上述流程,面试官大概率会顺着你的项目,切入到底层原理。以下是高频考点分类整理:
📝 考点 1:关于 Servlet 的基本功
Q1:Servlet 是单例的还是多例的?它是线程安全的吗?
- 答 :在 Tomcat 中默认是单例 的(路由表 HashMap 里只存了一个实例)。它不是线程安全的! 若多个用户同时访问,Tomcat 内部会开多线程去调用同一个对象的
service方法。如果我们在这个 Servlet 类里定义了全局成员变量,就会导致数据混乱。 - 如何解决:禁止使用全局变量,只在方法内部使用局部变量(局部变量在栈帧中,线程间隔离)。
Q2:说一下 Servlet 的生命周期?
- 答 :加载实例化 ->
init()初始化(只调用1次) ->service()处理请求(调用无数次) ->destroy()销毁(只调用1次)。
🌐 考点 2:关于并发模型与服务器架构 (重点拉开差距)
Q3:你写的这套 Tomcat 代码,它能扛住高并发吗?和真正的 Tomcat 有什么区别?
- 答(承认不足+指出原理) :我写的版本由于使用的是
ServerSocket.accept()以及在同一个线程里直接处理流,属于纯粹的 单线程 BIO (同步阻塞) 模型。同一时刻只能处理一个用户的请求,别人只能卡在外面等。 - 真正的 Tomcat :使用的是 多路复用 NIO + 线程池(多线程) 模型。有一个专门的 Acceptor 线程负责快速接收连接,然后将 I/O 读写和业务处理的工作扔进一个全局线程池(Worker 线程)中异步处理,所以在高并发下完全不会阻塞。
🔍 考点 3:关于 HTTP 协议
Q4:当你通过 InputStream 读取到的那一大串字符串(请求报文),它到底包含哪些结构?
- 答 :标准的 HTTP 请求报文包含四个切片:
- 请求行 :如
GET /first HTTP/1.1 - 请求头 (Headers) :各种键值对,比如
Host、User-Agent、Cookie - 空行:只有回车换行,用来分隔请求头和请求体。
- 请求体 (Body):如果是 POST 请求发送的表单数据或 JSON,就放在这里。
- 请求行 :如
Q5:GET 和 POST 到底有什么区别?
- 大白话总结 :GET 是向服务器**"索要/查询"数据,参数暴露在 URL 地址里,不安全,有长度限制;POST 是向服务器"提交/修改"**数据,参数藏在请求体 (Body) 里,相对安全,没有长度大小限制。
🧠 考点 4:关于设计模式与类加载器 (终极装逼点)
Q6:你知道 Tomcat 打破了双亲委派机制吗?为什么?
- 通俗解答(杀手锏):在 Java 中原本类加载是双亲委派(儿子先让父亲去加载,保证基础类不被篡改)。但是 Tomcat 作为一个容器,里面可能部署了两个不同的项目,项目 A 用了 Spring 3,项目 B 用了 Spring 5。如果用传统的双亲委派,就会导致只能加载一个版本的 Spring,发生版本冲突。
- 所以,Tomcat 自定义了
WebAppClassLoader(Web应用类加载器),它打破了委派机制,优先加载自己项目的WEB-INF/lib下的依赖。由于不同的项目有不同的类加载器,就实现了类之间的彻底隔离。
Q7:你在解析 @WebServlet 时使用到了哪种设计思想?
- 答 :体现了 控制反转 (IoC) 的思想。以前我们要用什么类都是开发者主动
new,但在 Tomcat 里,是我们贴上注解,框架(Tomcat 容器)负责把对象实例化,并把控制权接管过去,在必要的时候再通过"回调"我们的HashMap.get()+ 反射回调"执行我们的代码。
第二题:手写 MyTomcat 和"MyTomcat 线程池版本"的区别在哪里?
我帮您对比了这两个文件夹的核心源码,本质的区别在于:服务器处理并发请求的模型从"单线程的纯 BIO" 升级到了"伪异步的 BIO (配合线程池)"。
1. 版本一:普通版 MyTomcat(路边摊单人服务模式)
- 运行机制 :在主线程里,
serverSocket.accept()接收到一个浏览器的请求后,直接在当前线程往下执行 读取流、解析请求、处理 Servlet 业务的方法。处理完并断开 Socket 之后,主循环才会进入下一次accept()。 - 致命缺陷:如果你在代码里遇到一个很耗时的请求(比如下载文件花了10秒),在这 10 秒内,主线程被完全卡死。第二个、第三个人无论怎么发请求,Tomcat 都不会搭理,直接处于假死状态。
2. 版本二:MyTomcat 线程池版本(高级餐厅前后台分离模式)
这是企业级架构重构的第一步!在 myTomcat线程池版本 中,你新增了 HandleSocketServerPool.java 和 ServerThreadReader 类。
- 运行机制 :
- 你在启动时,创建了一个核心线程数2,最大线程数自定义,带阻塞队列的
ThreadPoolExecutor(线程池)。 - 主线程里的
while(true)现在只干一件事 :专门负责accept()迎客。拿到客人的Socket对象后,立马把他打包成一个Runnable任务对象 (ServerThreadReader),扔给线程池去处理。 - 然后主线程不管了,瞬间回头继续去门口等下一个客人。
- 你在启动时,创建了一个核心线程数2,最大线程数自定义,带阻塞队列的
- 核心优势总结 (面试点) :
- 应对高并发:现在哪怕有某个请求卡了 10 秒,它也只是占用了线程池里的一名"服务员"。主线程依然能极快地接收其他上百个并发请求,把它分发给别的服务员处理,Tomcat 不会阻塞了!
- 资源可控 :为什么不直接为每个请求
new Thread()而是用线程池?因为如果瞬间来 10000 个请求,直接 new 一万个线程会把内存撑爆(OOM)。利用通过你代码里的ArrayBlockingQueue可以把多出来的请求先放在队列里排队,起到了削峰填谷的缓冲作用。
装逼建议 :如果面试官问"这套代码还有没有优化空间?"
你可以回答:"虽然线程池版本解决了主线程阻塞排队的问题,但它依然是 BIO 阻塞流(
InputStream.read会卡死线程)。如果有 1 万个用户保持连接却不说话(比如做聊天室),就会把线程池的 1 万个线程全占满一直干等着。下一步的究极进化就是改成 **NIO(非阻塞)+ 多路复用(Selector)**的架构,这也是真实 Tomcat 8 之后和 Netty 所采用的终极方案。"