首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> J2ME

深入解析 Servlet 線程安全問題與最佳實踐

     編輯:J2ME

Servlet 的多線程機制

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


圖 1 Servlet 線程池

當多線程同時訪問同一 Servlet 實例並操作共享資源時,若未妥善處理線程安全,數據易發生不一致,導致難以排查的錯誤。

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 近乎同時訪問:

  • A: https://localhost:8080/servlet/ConcurrentTest?username=a
  • B: https://localhost:8080/servlet/ConcurrentTest?username=b

若 B 比 A 稍晚提交,A 的瀏覽器顯示空白,A 的資料卻出現在 B 的瀏覽器上(見圖 2)。


圖 2 A、B 用戶瀏覽器輸出對比

基於 Java 記憶體模型 (JMM) 的深度分析

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


圖 3 Servlet 實例 JMM 模型

根據圖 3,分析 A、B 兩線程併發執行時變量變化與線程調度(圖 4):

調度時刻A 線程B 線程
T1訪問 Servlet
T2訪問 Servlet
T3output=A輸出
username=a
休眠 5000ms,讓出 CPU
T4output=B輸出(寫回主存)
username=b
休眠 5000ms,讓出 CPU
T5在 B 瀏覽器輸出 A 的 username,A 終止
T6在 B 瀏覽器輸出 B 的 username,B 終止

圖 4 顯示:B 線程對實例變量 output 的修改覆蓋了 A 線程的修改,導致數據錯亂。此現象具偶然性,更增加隱患。

設計線程安全 Servlet 的三種方案

1. 實現 SingleThreadModel 接口

實現該接口後,service 方法不會被兩線程同時執行:

public class ConcurrentTest extends HttpServlet implements SingleThreadModel {
    // ...
}

缺點:容器為每請求創建實例,系統開銷大;Servlet 2.4 已不推薦使用。

2. 同步共享數據操作

用 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);
    }
}

缺點:同步塊同一時刻僅一線程執行,降低吞吐量,導致大量線程阻塞;頻繁刷新緩存影響性能。實際開發應避免或最小化同步代碼。

3. 避免使用實例變量(最佳選擇)

將實例變量改為方法局部變量:

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 應用的基石。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved