Servlet 容器預設對每個 Servlet 類別 僅實例化一個物件,所有請求執行緒共享此實例。這意味著:
| 類別 | 典型代表 | 執行緒安全 | 說明 |
|---|---|---|---|
| 類別變數 | application (ServletContext) | ⚠️ 非安全 | 全應用共享,需同步或用 ConcurrentHashMap |
| 請求範圍 | request, response, pageContext | ✅ 安全 | 每請求獨立建立 |
| 會話範圍 | session (HttpSession) | ⚠️ 非安全 | 同用戶併發請求共享,需同步 |
| 配置物件 | config (ServletConfig) | ✅ 安全 | 唯讀初始化參數 |
| 實例變數 | Servlet 類別的 private int count; | ❌ 危險 | 所有執行緒競爭讀寫 |
| 區域變數 | 方法內 String param = req.getParameter("x"); | ✅ 安全 | 堆疊封閉,無共享 |
public class CounterServlet extends HttpServlet {
private int visitCount = 0; // 實例變數=共享計數器
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
visitCount++; // 非原子操作:讀取→增加→寫回,競態條件必現
resp.getWriter().println("Visits: " + visitCount);
}
}
併發測試下,計數器會漏算、亂序,甚至出現負數(極端情況)。
protected void doGet(...) {
// 從資料庫/Redis/原子計數器讀取,不持有狀態
long count = atomicCounter.incrementAndGet();
resp.getWriter().println("Visits: " + count);
}
private ThreadLocal fmtLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
適用於「昂貴且非執行緒安全物件」的複用(如 SimpleDateFormat),但需防止記憶體洩漏(Tomcat 執行緒池復用)。
private final AtomicInteger counter = new AtomicInteger(0); // 推薦
// 或
synchronized(this) { visitCount++; } // 粗粒度鎖,降低吞吐
page、pageContext、request、response:請求級,安全。session、application:共享級,非安全,操作屬性時需同步。config:初始化級,唯讀,安全。final 欄位、集合不可變視圖)。AtomicLong、ConcurrentHashMap、LongAdder 取代 synchronized。「Servlet 單實例多執行緒」是 Java Web 開發者必須內化的核心心智模型。下次寫 private int x; 時,請自問:「這個變數會被百萬併發請求同時讀寫嗎?」 若答案是肯定,立即重構為區域變數或執行緒安全替代品。