bigywater.tech
Java · 371

Servlet 生命周期:实例、线程与请求边界

Servlet 规范里「一个实例,多线程」「请求对象只在一次请求内有效」「init 失败怎么办」这些说法,在真实容器里具体长什么样?这篇用嵌入式 Jetty 逐项实测。

1 个实例
64 个并发请求共用的 servlet 实例数,非同步 int 在 12.8 万次加法中丢失 186 次
500 → 404
servlet.init 抛 ServletException 后,第 1 次请求得到 500,之后一律 404,init 只调用 1 次
2041 → 601 ms
5 线程池处理 8 个各 500 ms 的请求:同步与异步(startAsync)的总耗时

启动、首次请求与停止的回调顺序

实测 · 本机执行输出
  [order]
  启动阶段(loadOnStartup:order0 = 0,order1 = 1,order2 = 2,lazy 不设置):
        [  200 ms][main] server.start() 之前
        [  218 ms][main] ServletContextListener.contextInitialized
        [  220 ms][main] S-lazy.init()
        [  220 ms][main] S-order0.init()
        [  220 ms][main] S-order1.init()
        [  220 ms][main] S-order2.init()
        [  227 ms][main] server.start() 返回
  第 1 次请求 /lazy(servlet 实例注册):200 S-lazy ok
  第 1 次请求 /bycls(按类注册,没设 loadOnStartup):200 LazyCls ok
  第 2 次请求 /bycls:200 LazyCls ok
  请求阶段:
        [  277 ms][qtp1295083508-N] S-lazy.service(GET /lazy)
        [  285 ms][qtp1295083508-N] LazyCls.init()
  请求不存在的路径 /nope:404 <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 404 Not Found</title> </head> <body> <h2>HTTP ERROR 404 Not Found</h2> <table> <tr><th>URI:</th><td>http://127.0.0.1:57096/nope</td></tr> <tr><th>STATUS:</th><td>404</td></tr> <tr><th>MESSAGE:</th><td>Not Found</td></tr> </table> <hr/><a href="https://jetty.org/">Powered by Jetty:// 12.0.16</a><hr/> </body> </html>
  停止阶段:
        [  291 ms][main] server.stop() 之前
        [  293 ms][main] S-order0.destroy()
        [  293 ms][main] S-lazy.destroy()
        [  293 ms][main] S-order1.destroy()
        [  293 ms][main] S-order2.destroy()
        [  293 ms][main] LazyCls.destroy()
        [  294 ms][main] ServletContextListener.contextDestroyed
        [  294 ms][main] server.stop() 返回

一个实例,多个线程

实测 · 本机执行输出
  [shared]
  64 个并发请求,每个把 4 个计数器各加 2000 次(期望值 128000),成功 64 个
      非同步 int:127814(丢失 186);volatile int:127807(丢失 193);synchronized int:128000;AtomicInteger:128000
      servlet 实例数(identityHashCode 的种类):1;init() 调用次数:1;处理请求的线程名种类:[qtpN-N]

请求对象的有效期

实测 · 本机执行输出
  [stale]
  请求 /first?k=A:200 uri=/first param=A
  请求完成之后,通过保存的引用读 getRequestURI():NullPointerException: Cannot invoke "org.eclipse.jetty.server.Request.getHttpURI()" because the return value of "org.eclipse.jetty.ee10.servlet.ServletApiRequest.getRequest()" is null
  读 getParameter("k"):A
  读 getMethod():NullPointerException: Cannot invoke "org.eclipse.jetty.server.Request.getMethod()" because the return value of "org.eclipse.jetty.ee10.servlet.ServletApiRequest.getRequest()" is null
  往保存的 response 里写:没有抛异常;isCommitted = IllegalStateException: Request/Response does not exist (likely recycled)
  再请求 /second?k=B:200 uri=/second param=B
  此时再读保存的第一个请求引用的 getRequestURI():NullPointerException: Cannot invoke "org.eclipse.jetty.server.Request.getHttpURI()" because the return value of "org.eclipse.jetty.ee10.servlet.ServletApiRequest.getRequest()" is null

init 失败

实测 · 本机执行输出
  [initfail]
  /fail 第 1 次请求(init 抛 ServletException):500 <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 500 jakarta.servlet.ServletException: init ??</title> </head> <body> <h2>HTTP ERROR 500 jakarta.servlet.ServletException: init ??</h2> <table> <tr><th>URI:</th><td>http://127.0.0.1:57216/fail</td></tr> <tr><th>STATUS:</th><td>500</td></tr> <tr><th>MESSAGE:</th><td>jakarta.servlet.ServletException: init ??</td></tr> </table> <hr/><a href="https://jetty.org/">Powered by Jetty:// 12.0.16</a><hr/> </body> </html>
  /fail 第 2 次请求(init 抛 ServletException):404 <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 404 Not Found</title> </head> <body> <h2>HTTP ERROR 404 Not Found</h2> <table> <tr><th>URI:</th><td>http://127.0.0.1:57216/fail</td></tr> <tr><th>STATUS:</th><td>404</td></tr> <tr><th>MESSAGE:</th><td>Not Found</td></tr> </table> <hr/><a href="https://jetty.org/">Powered by Jetty:// 12.0.16</a><hr/> </body> </html>
  /fail 第 3 次请求(init 抛 ServletException):404 <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 404 Not Found</title> </head> <body> <h2>HTTP ERROR 404 Not Found</h2> <table> <tr><th>URI:</th><td>http://127.0.0.1:57216/fail</td></tr> <tr><th>STATUS:</th><td>404</td></tr> <tr><th>MESSAGE:</th><td>Not Found</td></tr> </table> <hr/><a href="https://jetty.org/">Powered by Jetty:// 12.0.16</a><hr/> </body> </html>
    init 日志:
        [  276 ms][qtp1295083508-N] FailLazy.init() 第 1 次
  /temp(第 1 次 init 抛 UnavailableException,暂时 1 秒):503 <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 503 Service Unavailable</title> </head> <body> <h2>HTTP ERROR 503 Service Unavailable</h2> <table> <tr><th>URI:</th><td>http://127.0.0.1:57216/temp</td></tr> <tr><th>STATUS:</th><td>503</td></tr> <tr><th>MESSAGE:</th><td>Service Unavailable</td></tr> </table> <hr/><a href="https://jetty.org/">Powered by Jetty:// 12.0.16</a><hr/> </body> </html>
      立即再请求:503 <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 503 Service Unavailable</title> </head> <body> <h2>HTTP ERROR 503 Service Unavailable</h2> <table> <tr><th>URI:</th><td>http://127.0.0.1:57216/temp</td></tr> <tr><th>STATUS:</th><td>503</td></tr> <tr><th>MESSAGE:</th><td>Service Unavailable</td></tr> </table> <hr/><a href="https://jetty.org/">Powered by Jetty:// 12.0.16</a><hr/> </body> </html>
      1.3 秒之后再请求:200 temp ok
    init 日志:
        [  291 ms][qtp1295083508-N] TempLazy.init() 第 1 次
  /perm 第 1 次请求(UnavailableException 永久):404 <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 404 Not Found</title> </head> <body> <h2>HTTP ERROR 404 Not Found</h2> <table> <tr><th>URI:</th><td>http://127.0.0.1:57216/perm</td></tr> <tr><th>STATUS:</th><td>404</td></tr> <tr><th>MESSAGE:</th><td>Not Found</td></tr> </table> <hr/><a href="https://jetty.org/">Powered by Jetty:// 12.0.16</a><hr/> </body> </html>
  /perm 第 2 次请求(UnavailableException 永久):404 <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 404 Not Found</title> </head> <body> <h2>HTTP ERROR 404 Not Found</h2> <table> <tr><th>URI:</th><td>http://127.0.0.1:57216/perm</td></tr> <tr><th>STATUS:</th><td>404</td></tr> <tr><th>MESSAGE:</th><td>Not Found</td></tr> </table> <hr/><a href="https://jetty.org/">Powered by Jetty:// 12.0.16</a><hr/> </body> </html>
    init 日志:
        [ 1614 ms][qtp1295083508-N] PermLazy.init() 第 1 次
  另起一个服务器:loadOnStartup = 1 的 servlet 在 init 里抛 ServletException,server.start():失败 ServletException: 启动时失败
    日志:
        [ 1629 ms][main] S-other.init()
        [ 1629 ms][main] eager.init()(loadOnStartup = 1)
      server.isStarted() = false;isFailed() = true

停止时正在处理的请求

实测 · 本机执行输出
  [stop]
  客户端收到:ConnectException: null
  事件顺序:
        [  276 ms][qtp1295083508-N] slow.service 开始
        [  538 ms][main] server.stop() 调用(stopTimeout = 500 ms,请求还要约 1.7 s)
        [ 1061 ms][main] slow.destroy()
        [ 1573 ms][qtp1295083508-N] slow.service 被中断
        [ 1573 ms][qtp1295083508-N] slow.service 结束
        [ 1584 ms][main] server.stop() 抛 TimeoutException
        [ 1584 ms][main] server.stop() 返回(耗时 1045 ms)
  客户端收到:200 slow2 done
  stopTimeout 足够长时的事件顺序:
        [ 1591 ms][qtp20049680-N] slow2.service 开始
        [ 1899 ms][main] server.stop() 调用(stopTimeout = 5000 ms,请求还要约 0.7 s)
        [ 2591 ms][qtp20049680-N] slow2.service 结束
        [ 2599 ms][main] slow2.destroy()
        [ 2600 ms][main] server.stop() 返回(耗时 700 ms)

异步 servlet

实测 · 本机执行输出
  [async]
  线程池最多 5 个线程(其中一部分被 acceptor / selector 占用),8 个并发请求,每个耗时 500 ms,走 异步(startAsync):成功 8 个,总耗时 601 ms
  线程池最多 5 个线程(其中一部分被 acceptor / selector 占用),8 个并发请求,每个耗时 500 ms,走 同步(sleep):成功 8 个,总耗时 2041 ms
  单个异步请求的事件顺序:200 async done
  
        [ 2873 ms][qtp242355057-N] service 开始(startAsync)
        [ 2874 ms][qtp242355057-N] service 返回(请求线程释放)
        [ 3382 ms][my-async-pool] complete()

分步演示

Servlet 生命周期
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64,JDK 21.0.12;Jetty 12.0.16(ee10,jakarta.servlet-api 6.0.0)嵌入式启动,java.net.http.HttpClient 发请求;不同的容器(Tomcat / Undertow)在 init 失败重试、destroy 时机与请求对象回收上可能不同
    • 没有验证:Tomcat 与 Undertow 的同样实验、web.xml / @WebServlet 注解方式注册(这里用编程方式)、HttpSession 的生命周期与序列化、ServletContainerInitializer、虚拟线程执行器(JDK 21)对 servlet 线程模型的影响、HTTP/2 下的请求并发、request 属性与 AsyncContext.dispatch 的交互
    京ICP备2026067502号-1京公网安备11010502063487号