为什么Servlet至今仍让人困惑,以及如何应对?
\n\n如果你曾经在Stack Overflow上寻找过关于“Servlet是如何工作的?”这个问题的答案,那么你很可能遇到过那个拥有数千次浏览的经典帖子。那里反复被提出的问题是:“会创建多少个Servlet实例?如何在请求之间共享数据?为什么我的变量表现得像个疯子?”
\n\n老实说:Servlet并不是什么高深莫测的技术,但其中确实有不少陷阱。尤其是当涉及到多线程和共享变量的时候。在本文中,我们不仅仅是复述文档。我们将采用一个真实的例子,对其进行深入剖析,并展示如何避免踩坑。开始吧!
\n\nServlet如何进入内存:实例化与生命周期
\n\n让我们从简单的问题开始:会创建多少个Servlet实例?答案是:一个。是的,你没听错。Servlet容器(如Tomcat、Jetty等)为整个应用程序只创建一个Servlet实例。这个实例会一直存在,直到应用程序被卸载。
\n\n典型的生命周期如下所示:
\n\n- \n
init()— 在Servlet加载时调用一次。在这里初始化数据库连接、配置等。 \n service()— 为每个传入的请求调用。这里进行实际的请求处理。 \n destroy()— 在Servlet卸载前调用。清理资源。 \n
关键点在于:service()(以及它的派生方法doGet、doPost)会在每个请求的独立线程中执行。也就是说,同一个Servlet实例同时处理数百个请求。而有趣的事情就从这里开始。
示例:一个Servlet,多个线程
\n\n@WebServlet("/counter")\npublic class CounterServlet extends HttpServlet {\n private int counter = 0; // 共享变量!\n\n @Override\n protected void doGet(HttpServletRequest req, HttpServletResponse resp) \n throws ServletException, IOException {\n counter++;\n resp.getWriter().write("Counter: " + counter);\n }\n}\n\n如果你运行这段代码并发送10个并行请求,你不会按顺序得到从1到10的数字。相反,你会看到类似这样的结果:2, 5, 3, 8, 1... 或者counter可能会跳过某些值。为什么?因为counter++不是一个原子操作。两个线程可能读取到相同的值,各自递增后再写回,从而导致一次递增丢失。
结论:Servlet实例的字段是共享变量。它们对所有线程可见。如果你不同步对它们的访问,就会发生竞态条件。
\n\n会话:如何不混淆用户
\n\n现在想象一下:你想要存储每个用户独有的数据(购物车、登录信息、设置等)。为此,Java Servlets提供了HttpSession。会话会为每个新用户创建,并一直存在,直到用户关闭浏览器或超时。
\n\n会话是非共享内存。每个用户获得自己独立的会话实例。但在会话内部,你可以存储任何对象,这些对象仅对该用户可用。
\n\n示例:使用会话
\n\n@WebServlet("/session-demo")\npublic class SessionServlet extends HttpServlet {\n @Override\n protected void doGet(HttpServletRequest req, HttpServletResponse resp) \n throws ServletException, IOException {\n HttpSession session = req.getSession();\n Integer visitCount = (Integer) session.getAttribute("visitCount");\n if (visitCount == null) {\n visitCount = 0;\n