Servlet 架構建立在 Java 多線程機制之上,其生命週期由 Web 容器管理。當客戶端首次請求某個 Servlet 時,容器根據 web.xml 實例化該 Servlet 類。後續請求不會重新實例化,而是由多線程共享同一實例。容器透過線程池支撐系統運行,如圖 1 所示。

當多線程同時訪問同一 Servlet 實例並操作共享資源時,若未妥善處理線程安全,數據易發生不一致,導致難以排查的錯誤。
線程安全問題主要源於實例變量使用不當。以下例說明:
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
public class ConcurrentTest extends HttpServlet {
PrintWriter output; // 實例變量
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username;
response.setContentType("text/html; charset=gb2312");
username = request.getParameter("username");
output = response.getWriter();
try {
Thread.sleep(5000); // 延時放大併發問題
} catch (InterruptedException e) {}
output.println("用戶名:" + username);
}
}
該 Servlet 定義實例變量 output,單用戶訪問正常,但多用戶併發時,output 被覆寫,導致用戶 A 的資料顯示在用戶 B 的瀏覽器上。為觀察現象,代碼加入 5 秒延時。假設用戶 A、B 近乎同時訪問:
https://localhost:8080/servlet/ConcurrentTest?username=ahttps://localhost:8080/servlet/ConcurrentTest?username=b若 B 比 A 稍晚提交,A 的瀏覽器顯示空白,A 的資料卻出現在 B 的瀏覽器上(見圖 2)。

JMM 規定:主內存存放所有實例變量,線程共享;每線程擁有工作內存(緩存+堆棧),緩存存放主內存變量副本,可能不同步;堆棧存放局部變量,線程間不可互訪。Servlet 實例的 JMM 模型如圖 3 所示。

根據圖 3,分析 A、B 兩線程併發執行時變量變化與線程調度(圖 4):
| 調度時刻 | A 線程 | B 線程 |
|---|---|---|
| T1 | 訪問 Servlet | |
| T2 | 訪問 Servlet | |
| T3 | output=A輸出username=a休眠 5000ms,讓出 CPU | |
| T4 | output=B輸出(寫回主存)username=b休眠 5000ms,讓出 CPU | |
| T5 | 在 B 瀏覽器輸出 A 的 username,A 終止 | |
| T6 | 在 B 瀏覽器輸出 B 的 username,B 終止 |
圖 4 顯示:B 線程對實例變量 output 的修改覆蓋了 A 線程的修改,導致數據錯亂。此現象具偶然性,更增加隱患。
實現該接口後,service 方法不會被兩線程同時執行:
public class ConcurrentTest extends HttpServlet implements SingleThreadModel {
// ...
}
缺點:容器為每請求創建實例,系統開銷大;Servlet 2.4 已不推薦使用。
用 synchronized 保護關鍵區段:
public class ConcurrentTest extends HttpServlet {
// ...
username = request.getParameter("username");
synchronized (this) {
output = response.getWriter();
try { Thread.sleep(5000); } catch (InterruptedException e) {}
output.println("用戶名:" + username);
}
}
缺點:同步塊同一時刻僅一線程執行,降低吞吐量,導致大量線程阻塞;頻繁刷新緩存影響性能。實際開發應避免或最小化同步代碼。
將實例變量改為方法局部變量:
public class ConcurrentTest extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
PrintWriter output; // 局部變量
String username;
response.setContentType("text/html; charset=gb2312");
// ...
}
}
局部變量分配在棧上,每線程私有,天然線程安全。JMM 確認:棧內變量不共享,無安全隱患。
Servlet 線程安全問題僅在高併發下顯現,極難發現。核心原因在於實例變量,故開發中應堅決避免使用實例變量。若設計無法避免,可用同步保護,但需同步最小代碼路徑以保性能。良好的編碼習慣是構建健壯 Web 應用的基石。