聊聊Servlet、Struts1、Struts2以及SpringMvc中的執行緒安全
前言
很多初學者,甚至是工作1-3年的小夥伴們都可能弄不明白?servlet Struts1 Struts2 springmvc 哪些是單例,哪些是多例,哪些是執行緒安全?
在談這個話題之前,我們先了解一下Java中相關的變數型別以及記憶體模型JMM。
變數型別
- 類變數:獨立於方法之外的變數,用 static 修飾。
- 區域性變數:類的方法中的變數。
- 例項變數(全域性變數):獨立於方法之外的變數,不過沒有 static 修飾。
JAVA的區域性變數
- 區域性變數宣告在方法、構造方法或者語句塊中;
- 區域性變數在方法、構造方法、或者語句塊被執行的時候建立,當它們執行完成後,變數將會被銷燬;
- 訪問修飾符不能用於區域性變數;
- 區域性變數只在宣告它的方法、構造方法或者語句塊中可見;
- 區域性變數是在棧上分配的。
- 區域性變數沒有預設值,所以區域性變數被聲明後,必須經過初始化,才可以使用。
JAVA的例項變數
- 例項變數宣告在一個類中,但在方法、構造方法和語句塊之外;
- 當一個物件被例項化之後,每個例項變數的值就跟著確定;
- 例項變數在物件建立的時候建立,在物件被銷燬的時候銷燬;
- 例項變數的值應該至少被一個方法、構造方法或者語句塊引用,使得外部能夠通過這些方式獲取例項變數資訊;
- 例項變數可以宣告在使用前或者使用後;
- 訪問修飾符可以修飾例項變數;
- 例項變數對於類中的方法、構造方法或者語句塊是可見的。一般情況下應該把例項變數設為私有。通過使用訪問修飾符可以使例項變數對子類可見;
- 例項變數具有預設值。數值型變數的預設值是0,布林型變數的預設值是false,引用型別變數的預設值是null。變數的值可以在宣告時指定,也可以在構造方法中指定;例項變數可以直接通過變數名訪問。但在靜態方法以及其他類中,就應該使用完全限定名:ObejectReference.VariableName。
JAVA的類變數(靜態變數)
- 類變數也稱為靜態變數,在類中以static關鍵字宣告,但必須在方法構造方法和語句塊之外。
- 無論一個類建立了多少個物件,類只擁有類變數的一份拷貝。
- 靜態變數除了被宣告為常量外很少使用。常量是指宣告為public/private,final和static型別的變數。常量初始化後不可改變。
- 靜態變數儲存在靜態儲存區。經常被宣告為常量,很少單獨使用static宣告變數。
- 靜態變數在程式開始時建立,在程式結束時銷燬。
- 與例項變數具有相似的可見性。但為了對類的使用者可見,大多數靜態變數宣告為public型別。
- 預設值和例項變數相似。數值型變數預設值是0,布林型預設值是false,引用型別預設值是null。變數的值可以在宣告的時候指定,也可以在構造方法中指定。此外,靜態變數還可以在靜態語句塊中初始化。
- 靜態變數可以通過:ClassName.VariableName的方式訪問。
- 類變數被宣告為public static final型別時,類變數名稱一般建議使用大寫字母。如果靜態變數不是public和final型別,其命名方式與例項變數以及區域性變數的命名方式一致。
Java的記憶體模型JMM
Java的記憶體模型JMM(Java Memory Model)JMM主要是為了規定了執行緒和記憶體之間的一些關係。根據JMM的設計,系統存在一個主記憶體(Main Memory),Java中所有例項變數都儲存在主存中,對於所有執行緒都是共享的。每條執行緒都有自己的工作記憶體(Working Memory),工作記憶體由快取和堆疊兩部分組成,快取中儲存的是主存中變數的拷貝,快取可能並不總和主存同步,也就是快取中變數的修改可能沒有立刻寫到主存中;堆疊中儲存的是執行緒的區域性變數,執行緒之間無法相互直接訪問堆疊中的變數。根據JMM,我們可以將論文中所討論的Servlet例項的記憶體模型抽象為下圖所示的模型。
執行緒安全
Servlet
Servlet/JSP技術和ASP、PHP等相比,由於其多執行緒執行而具有很高的執行效率。由於Servlet/JSP預設是以多執行緒模式執行的,所以,在編寫程式碼時需要非常細緻地考慮多執行緒的安全性問題。然而,很多人編寫Servlet/JSP程式時並沒有注意到多執行緒安全性的問題,這往往造成編寫的程式在少量使用者訪問時沒有任何問題,而在併發使用者上升到一定值時,就會經常出現一些莫明其妙的問題。
Servlet的多執行緒機制
Servlet體系結構是建立在Java多執行緒機制之上的,它的生命週期是由Web容器負責的。當客戶端第一次請求某個Servlet 時,Servlet容器將會根據web.xml配置檔案例項化這個Servlet類。當有新的客戶端請求該Servlet時,一般不會再例項化該 Servlet類,也就是有多個執行緒在使用這個例項。Servlet容器會自動使用執行緒池等技術來支援系統的執行,如下圖所示。
這樣,當兩個或多個執行緒同時訪問同一個Servlet時,可能會發生多個執行緒同時訪問同一資源的情況,資料可能會變得不一致。所以在用Servlet構建的Web應用時如果不注意執行緒安全的問題,會使所寫的Servlet程式有難以發現的錯誤。
Servlet的執行緒安全問題
Servlet的執行緒安全問題主要是由於例項變數使用不當而引起的,這裡以一個現實的例子來說明。
/** * 模擬使用者AB在同時執行不同的動作 * 先執行 http://localhost:8080/concurrent?username=A&action=play * 稍後執行 http://localhost:8080/concurrent?username=B&action=eat */
public class Concurrent extends HttpServlet {
private static final long serialVersionUID = 1L;
private String action = "";//動作
publicvoiddoGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
String username = request.getParameter("username");
action = request.getParameter("username");
Thread.sleep(5000); //為了突出併發問題,在這設定一個延時
//如果不出意外,應該使用者AB都在吃飯
System.out.println("使用者:"+username+"在"+action);
} catch (Exception e) {
}
}
}
Struts1
首先,明確一點Sturts1 action是單例模式,執行緒是不安全的。Struts1使用的ActionServlet是單例的,既然是單例,當使用例項變數的時候就會有執行緒安全的問題。所有一般在開發中試禁止使用例項變數的。
Struts2
struts2使用的是actionContext,都是使用裡面的例項變數,讓struts2自動匹配成物件的。每次處理一個請求,struts2就會例項化一個物件,這樣就不會有執行緒安全的問題了。
需要注意的是,如果struts2+spring來管理注入的時候,不要把Action設定成單例,否則會出問題的。當然現在很少有專案使用struts2了。
SpringMVC
SpringMVC的controller預設是單例模式的,所以也會有多執行緒併發的問題。
總結
servlet Struts1 SpringMvc 是執行緒不安全的,當然如果你不使用例項變數也就不存線上程安全的問題了。
Struts2 是執行緒安全的,當然前提情況是,Action 不交給 spring管理,並且不設定為單例。
SpringMvc 的 Bean 可以設定成多例變成執行緒安全,但是一定程度上回影響系統性能。