Servlet 容器默認採用多線程模型處理請求:單一 Servlet 實例服務於所有並發請求。這雖然極大提升了吞吐量,但也引入了經典的線程安全隱患——實例變量在多線程間共享,若操作不當會導致數據錯亂。本文通過具體案例、Java 內存模型(JMM)分析,系統闡述線程不安全的根因,並對比三種解決方案的優劣,給出生產環境推薦策略。
service() 方法public class UnsafeServlet extends HttpServlet {
private PrintWriter output; // 實例變量 —— 共享變量!
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String username = req.getParameter("username");
output = resp.getWriter(); // 線程 A 寫入,線程 B 覆蓋
try { Thread.sleep(5000); } catch (InterruptedException ignored) {}
output.println("用戶名: " + username); // 可能輸出錯誤用戶名
}
}
現象:用戶 A 請求,睡眠期間用戶 B 請求覆蓋 output,導致 A 的瀏覽器顯示 B 的數據,甚至 A 看到空白頁。
根據 Java 內存模型:
output),所有線程共享時序圖解:
| 時刻 | 線程 A (用戶A) | 線程 B (用戶B) |
|---|---|---|
| T1 | 讀取 Servlet | |
| T2 | 讀取 Servlet | |
| T3 | output = A的writersleep 讓出 CPU | |
| T4 | output = B的writer (覆蓋主內存)sleep 讓出 CPU | |
| T5 | 輸出 username=A → 實際用 B 的 writer 輸出到 B 瀏覽器 | |
| T6 | 輸出 username=B → B 瀏覽器顯示兩行 |
核心問題:實例變量 output 非線程安全發布,且操作非原子。
public class SafeServlet extends HttpServlet implements SingleThreadModel { ... }
synchronized (this) {
output = resp.getWriter();
Thread.sleep(5000);
output.println("用戶名: " + username);
}
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
PrintWriter output = resp.getWriter(); // 局部變量,線程私有
String username = req.getParameter("username");
try { Thread.sleep(5000); } catch (InterruptedException ignored) {}
output.println("用戶名: " + username);
}
ThreadLocal、請求域、Session、Redis 等外部存儲| 方案 | 並發性能 | 代碼複雜度 | 維護性 | 推薦指數 |
|---|---|---|---|---|
| SingleThreadModel | 極差 | 低 | 差(已棄用) | ⭐ |
| 同步塊 | 差(串行) | 低 | 中(易死鎖) | ⭐⭐ |
| 無實例變量/局部變量 | 優(無鎖) | 低 | 優 | ⭐⭐⭐⭐⭐ |
| ThreadLocal/請求域 | 優 | 中 | 良 | ⭐⭐⭐⭐ |
ThreadLocal 隔離,或引入外部併發安全存儲掌握 Servlet 線程模型與 JMM,堅持「無狀態 Servlet + 局部變量」原則,是編寫高性能、穩定 Web 應用的基石。