天天看點

深入研究Servlet線程安全性問題

  摘 要:介紹了Servlet多線程機制,通過一個執行個體并結合Java 的記憶體模型說明引起Servlet線程不安全的原因,給出了保證Servlet線程安全的三種解決方案,并說明三種方案在實際開發中的取舍。

  關鍵字:Servlet 線程安全 同步 Java記憶體模型 執行個體變量

  Servlet/JSP技術和ASP、PHP等相比,由于其多線程運作而具有很高的執行效率。由于Servlet/JSP預設是以多線程模式執行的,是以,在編寫代碼時需要非常細緻地考慮多線程的安全性問題。然而,很多人編寫Servlet/JSP程式時并沒有注意到多線程安全性的問題,這往往造成編寫的程式在少量使用者通路時沒有任何問題,而在并發使用者上升到一定值時,就會經常出現一些莫明其妙的問題。

  Servlet的多線程機制

 

  Servlet體系結構是建立在Java多線程機制之上的,它的生命周期是由Web容器負責的。當用戶端第一次請求某個Servlet時,Servlet容器将會根據web.xml配置檔案執行個體化這個Servlet類。當有新的用戶端請求該Servlet時,一般不會再執行個體化該Servlet類,也就是有多個線程在使用這個執行個體。Servlet容器會自動使用線程池等技術來支援系統的運作,如圖1所示。

圖1 Servlet線程池

  這樣,當兩個或多個線程同時通路同一個Servlet時,可能會發生多個線程同時通路同一資源的情況,資料可能會變得不一緻。是以在用Servlet建構的Web應用時如果不注意線程安全的問題,會使所寫的Servlet程式有難以發現的錯誤。

  Servlet的線程安全問題

  Servlet的線程安全問題主要是由于執行個體變量使用不當而引起的,這裡以一個現實的例子來說明。

Import javax.servlet. *;

Import javax.servlet.http. *;

Import java.io. *;

Public class Concurrent Test 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 (Interrupted Exception e){}

output.println("使用者名:"+Username+"<BR>");

}

  該Servlet中定義了一個執行個體變量output,在service方法将其指派為使用者的輸出。當一個使用者通路該Servlet時,程式會正常的運作,但當多個使用者并發通路時,就可能會出現其它使用者的資訊顯示在另外一些使用者的浏覽器上的問題。這是一個嚴重的問題。為了突出并發問題,便于測試、觀察,我們在回顯使用者資訊時執行了一個延時的操作。假設已在web.xml配置檔案中注冊了該Servlet,現有兩個使用者a和b同時通路該Servlet(可以啟動兩個IE浏覽器,或者在兩台機器上同時通路),即同時在浏覽器中輸入:

  a: http://localhost: 8080/servlet/ConcurrentTest? Username=a

  b: http://localhost: 8080/servlet/ConcurrentTest? Username=b

  如果使用者b比使用者a回車的時間稍慢一點,将得到如圖2所示的輸出:

圖2 a使用者和b使用者的浏覽器輸出

  從圖2中可以看到,Web伺服器啟動了兩個線程分别處理來自使用者a和使用者b的請求,但是在使用者a的浏覽器上卻得到一個空白的螢幕,使用者a的資訊顯示在使用者b的浏覽器上。該Servlet存線上程不安全問題。下面我們就從分析該執行個體的記憶體模型入手,觀察不同時刻執行個體變量output的值來分析使該Servlet線程不安全的原因。

  Java的記憶體模型JMM(Java Memory Model)JMM主要是為了規定了線程和記憶體之間的一些關系。根據JMM的設計,系統存在一個主記憶體(Main Memory),Java中所有執行個體變量都儲存在主存中,對于所有線程都是共享的。每條線程都有自己的工作記憶體(Working Memory),工作記憶體由緩存和堆棧兩部分組成,緩存中儲存的是主存中變量的拷貝,緩存可能并不總和主存同步,也就是緩存中變量的修改可能沒有立刻寫到主存中;堆棧中儲存的是線程的局部變量,線程之間無法互相直接通路堆棧中的變量。根據JMM,我們可以将論文中所讨論的Servlet執行個體的記憶體模型抽象為圖3所示的模型。

圖3 Servlet執行個體的JMM模型

  下面根據圖3所示的記憶體模型,來分析當使用者a和b的線程(簡稱為a線程、b線程)并發執行時,Servlet執行個體中所涉及變量的變化情況及線程的執行情況,如圖4所示。

排程時刻 a線程 b線程
T1 通路Servlet頁面
T2
T3 output=a的輸出username=a休眠5000毫秒,讓出CPU
T4 output=b的輸出(寫回主存)username=b休眠5000毫秒,讓出CPU
T5 在使用者b的浏覽器上輸出a線程的username的值,a線程終止。
T6 在使用者b的浏覽器上輸出b線程的username的值,b線程終止。

                  圖4 Servlet執行個體的線程排程情況

  從圖4中可以清楚的看到,由于b線程對執行個體變量output的修改覆寫了a線程對執行個體變量output的修改,進而導緻了使用者a的資訊顯示在了使用者b的浏覽器上。如果在a線程執行輸出語句時,b線程對output的修改還沒有重新整理到主存,那麼将不會出現圖2所示的輸出結果,是以這隻是一種偶然現象,但這更增加了程式潛在的危險性。