← 返回题目列表

Servlet 的生命周期是什么?Servlet 是线程安全的吗?

高频 中等 第 7 / 23 题 更新于 2026/07/25
Servlet生命周期线程安全Tomcat

简化版

Servlet 的生命周期由容器管理,分四步:加载实例化 → init() 初始化(只调用一次)→ 并发调用 service() 处理请求(多次)→ destroy() 销毁(只调用一次)Servlet 默认是「单实例多线程」——容器为一个 Servlet 只创建一个实例,多个请求由线程池的不同线程并发调用同一个实例。所以 Servlet 不是线程安全的:如果把请求级的可变状态(如当前用户、请求参数)存到成员变量,会被并发请求互相覆盖。局部变量(在线程栈上)是安全的。

详细版

Servlet 生命周期四阶段:

阶段方法次数说明
实例化构造方法1 次容器创建实例(默认懒加载,首次访问时;可配 load-on-startup 启动时创建)
初始化init()1 次读配置、准备共享资源
服务service()doGet/doPost多次每个请求调用一次,多线程并发
销毁destroy()1 次应用卸载/关闭时,释放资源

线程安全的核心——单实例多线程:

public class BadServlet extends HttpServlet {
    private String username;   // ❌ 成员变量!多线程共享,会串号

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        username = req.getParameter("username");  // 线程 A 刚设置
        // ... 线程 B 可能在这里把 username 改了
        resp.getWriter().write(username);          // A 输出的可能是 B 的值!
    }
}
public class GoodServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String username = req.getParameter("username");  // ✅ 局部变量,线程私有
        resp.getWriter().write(username);
    }
}

什么安全、什么不安全:

  • 局部变量(方法内,线程栈私有)、不可变对象无状态的组件线程安全集合
  • 成员变量存请求级可变状态静态可变变量非线程安全工具类作为字段共享(如 SimpleDateFormat)。

完整版教学

一、生命周期由容器管理,不要自己 new

Servlet 是运行在容器(Tomcat)里的组件,它的创建、初始化、调用、销毁全部由容器负责,业务代码不应该手动 new Servlet()。容器通过部署描述(web.xml)或注解(@WebServlet)发现 Servlet,然后:

  • 实例化:容器创建 Servlet 实例。默认是懒加载(第一次被访问时才创建),也可以用 load-on-startup 配置成启动时就创建(适合初始化重、希望提前暴露问题的 Servlet)。
  • init():实例化后立即调用一次,用于初始化——读配置参数、准备共享资源(如连接池、模板引擎)。
  • service()每个请求调用一次,内部根据 HTTP 方法分发到 doGet/doPost 等。这是唯一会被多次、并发调用的方法。
  • destroy():应用卸载或容器关闭时调用一次,用于释放资源。

开发者只关注这些回调逻辑,不控制对象的创建销毁时机。

二、核心:单实例多线程模型

这是理解 Servlet 线程安全的关键。 容器对一个 Servlet 默认只创建一个实例,然后:

  • 多个客户端请求到来时,容器从线程池取不同的 Worker 线程并发地调用同一个 Servlet 实例service() 方法。
  • 也就是说,同一个 Servlet 对象被多个线程同时执行

为什么这样设计?减少对象创建开销——不用每个请求都 new 一个 Servlet。但代价是:这个共享的 Servlet 实例,它的成员变量(字段)就成了多线程共享的临界区——多个线程同时读写,就有线程安全问题。

位置是否共享是否适合保存请求数据原因
方法局部变量否,每个线程栈帧独立适合每个请求线程都有自己的局部变量副本
Servlet 成员变量是,同一个 Servlet 实例共享不适合多个请求会并发读写同一个字段
static 变量是,应用级甚至 JVM 级共享更不适合影响范围更大,跨实例共享
不可变对象字段是,但不可修改可以没有并发写入,不会串号

记忆钩子:Servlet 只有一个对象,但有很多线程同时进它的方法;方法栈是各走各的,字段是大家共用的。

三、哪些状态安全、哪些不安全

安全的:

  • 方法内的局部变量:每个线程有自己的栈帧,局部变量在各自栈上,线程私有、天然安全。这是最推荐的做法——请求相关的数据用局部变量。
  • 不可变对象(final、String 等):不能被修改,共享也安全。
  • 无状态的对象:本身不保存可变状态(如无状态的 Service)。
  • 线程安全的组件:如 ConcurrentHashMap、原子类。

不安全的:

  • 成员变量保存请求级可变状态:把当前用户、请求参数、临时计算结果存到 Servlet 字段——多个请求会互相覆盖
  • 静态可变变量:全局共享,更危险。
  • 非线程安全工具类当字段共享:典型是 SimpleDateFormat(它内部有可变状态,多线程共用会解析出错乱结果)、StringBuilder 等。

四、典型的线程安全 bug(隐蔽且难复现)

最经典的错误是把请求数据放到成员变量

private String username;   // 字段
doGet() {
    username = req.getParameter("username");  // 线程 A 设为 "Alice"
    // 时间片切换,线程 B 进来把 username 设为 "Bob"
    doSomething();
    resp.write(username);  // 线程 A 这里输出的却是 "Bob"!—— 串号
}

这种 bug 的可怕之处:低并发下几乎不出现,单机手动测试根本测不出来,只在线上高并发或压测时偶发——用户看到别人的信息、响应内容错乱、数据串号。排查极难。所以从写代码时就要杜绝:请求级状态一律用局部变量,绝不放字段

SimpleDateFormat 是另一个高频坑——它不是线程安全的,作为 Servlet/Spring Bean 的字段被多线程共用会解析出错误日期。解决:用局部变量、ThreadLocal、或换 java.time.DateTimeFormatter(线程安全)。

T1(Alice): username = "Alice"
T1(Alice): 切出,尚未 write
T2(Bob)  : username = "Bob"
T2(Bob)  : write("Bob")
T1(Alice): 切回,write(username) -> 读到 "Bob"

这个时间线里没有任何 Tomcat bug,问题完全来自共享字段。假设 1000 次请求里只有 1 次刚好在两行代码之间切换,单测也很难稳定复现,但线上并发越高,概率越大。

五、和 Spring Bean 线程安全的一致性

这个原则可以直接迁移到 SpringSpring MVC 的 @Controller@Service 默认也是单例(singleton),多线程共享——和 Servlet 单实例多线程一模一样。所以:

  • Controller / Service 应尽量无状态:不要在字段里存请求级可变数据。
  • 请求状态放方法参数、局部变量里。
  • 如果确实需要「每个请求一个实例」,可以把 Bean 的 scope 改成 prototyperequest(但一般不推荐,无状态设计更好)。

一句话:Servlet 单例多线程 → Controller 单例多线程,线程安全的关键都是「不要用共享的可变字段存请求状态」

六、init 和 destroy 适合做什么

  • init() 适合:读取配置、初始化共享且线程安全的资源(连接池、缓存、模板引擎)。注意不要在 init 里做过长的阻塞操作(会拖慢启动),也不要把请求相关数据初始化成全局状态
  • destroy() 适合:释放资源——关闭连接池、停止后台线程、清理 ThreadLocal(防止前面讲的类加载器泄漏)。

七、排查线程安全问题的思路

遇到偶发的串号、用户信息错乱、响应内容互相污染、日期解析乱等现象:

  1. 优先怀疑共享的可变状态——检查 Servlet/Controller 的成员变量、静态变量里有没有存请求级数据。
  2. 检查非线程安全工具类是否被当字段共享(SimpleDateFormat 是重灾区)。
  3. 记住:线程安全问题的根源不是 Servlet API 本身,而是「共享可变状态」。把状态改成局部变量或线程安全组件,问题就解决了。

八、常见误区与追问

  • 误区:Servlet 每个请求都会创建一个新对象。 默认不是,容器通常为一个 Servlet 创建单实例,多请求由线程池并发调用同一个实例。
  • 误区:成员变量加 private 就线程安全。 private 只限制访问范围,不改变多个线程共享同一字段的事实。
  • 追问:为什么局部变量是安全的? 局部变量存在各线程自己的栈帧中,请求之间没有共享同一个变量槽位。
  • 追问:init() 里初始化连接池可以吗? 可以初始化共享且线程安全的资源,但不能把请求级状态放进去,也要避免过长阻塞拖慢启动。
  • 误区:SimpleDateFormat 作为字段复用能提升性能且没风险。 它内部有可变状态,多线程共用会错乱,应该用 DateTimeFormatter 或每次创建/ThreadLocal 并清理。
  • 追问:Spring Controller 为什么也要无状态? Controller 默认是单例 Bean,多线程并发访问,和 Servlet 单实例多线程模型的风险一致。

九、加强记忆

Servlet 生命周期由容器管理实例化 → init()(1 次,初始化共享资源)→ service()(多次、多线程并发,分发到 doGet/doPost)→ destroy()(1 次,释放资源),业务代码别自己 new。核心是**「单实例多线程」——容器只创建一个 Servlet 实例**,多请求用不同线程并发调用同一实例,所以 Servlet 不是线程安全的成员变量/静态变量存请求级可变状态会被并发覆盖(串号)局部变量在线程栈上才安全。典型坑:字段存请求数据、SimpleDateFormat 当字段共享——低并发测不出、高并发才暴露。原则同 Spring(@Controller/@Service 默认单例多线程):无状态、请求状态放局部变量。排查串号:先查共享可变字段。