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


本文參考鏈接(略加改動):http://www.yesky.com/334/1951334.shtml

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

 

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

 

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

                                       

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

  Servlet的線程安全問題

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

 1 public class ConcurrentTest extends HttpServlet {
 2     PrintWriter output;
 3     @Override
 4     protected void service(HttpServletRequest request, HttpServletResponse response)
 5             throws ServletException, IOException {
 6         String  username;
 7         response.setContentType("text/html;charset=gb2312");
 8         username=request.getParameter("username");
 9         output=response.getWriter();
10         try {
11             //為了突出並發問題,在這設置一個延時
12             Thread.sleep(5000);
13             output.println("用戶名:"+username+"<BR>"); 
14         } catch (Exception e) {
15             e.printStackTrace();
16         }
17     }
18 }

  該Servlet中定義了一個實例變量output,在service方法將其賦值為用戶的輸出。當一個用戶訪問該Servlet時,程序會正常的運行,但當多個用戶並發訪問時,就可能會出現其它用戶的信息顯示在另外一些用戶的瀏覽器上的問題。這是一個嚴重的問題。為了突出並發問題,便於測試、觀察,我們在回顯用戶信息時執行了一個延時的操作。假設已在web.xml配置文件中注冊了該Servlet,現有兩個用戶a和b同時訪問該Servlet(可以啟動兩個IE瀏覽器,或者在兩台機器上同時訪問),即同時在瀏覽器中輸入:

  a: http://localhost:8080/ServletTest/ConcurrentTest?Username=a
  b: http://localhost:8080/ServletTest/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所示的內存模型,來分析當用戶a和b的線程(簡稱為a線程、b線程)並發執行時,Servlet實例中所涉及變量的變化情況及線程的執行情況,如圖4所示。

           

  從圖4中可以清楚的看到,由於b線程對實例變量output的修改覆蓋了a線程對實例變量output的修改,從而導致了用戶a的信息顯示在了用戶b的瀏覽器上。如果在a線程執行輸出語句時,b線程對output的修改還沒有刷新到主存,那么將不會出現圖2所示的輸出結果,因此這只是一種偶然現象,但這更增加了程序潛在的危險性。

   設計線程安全的Servlet

  通過上面的分析,我們知道了實例變量不正確的使用是造成Servlet線程不安全的主要原因。下面針對該問題給出了三種解決方案並對方案的選取給出了一些參考性的建議。

  1、實現 SingleThreadModel 接口

  該接口指定了系統如何處理對同一個Servlet的調用。如果一個Servlet被這個接口指定,那么在這個Servlet中的service方法將不會有兩個線程被同時執行,當然也就不存在線程安全的問題。這種方法只要將前面的Concurrent Test類的類頭定義更改為:

1 public class ConcurrentTest extends HttpServlet implements SingleThreadModel  {
2       ...  ...      
3 }

  javax.servlet.SingleThreadModel API及其翻譯

  Ensures that servlets handle only one request at a time. This interface has no methods.

  確保servlet每次只處理一項請求。接口不含方法。

  If a servlet implements this interface, you are guaranteed that no two threads will execute concurrently in the servlet's service method. The servlet container can make this guarantee by synchronizing access to a single instance of the servlet, or by maintaining a pool of servlet instances and dispatching each new request to a free servlet.

  如果servlet實現了該接口,會確保不會有兩個線程同時執行servlet的service方法。 servlet容器通過同步化訪問servlet的單實例來保證,也可以通過維持servlet的實例池,對於新的請求會分配給一個空閑的servlet。

  Note that SingleThreadModel does not solve all thread safety issues. For example, session attributes and static variables can still be accessed by multiple requests on multiple threads at the same time, even when SingleThreadModel servlets are used. It is recommended that a developer take other means to resolve those issues instead of implementing this interface, such as avoiding the usage of an instance variable or synchronizing the block of the code accessing those resources. This interface is deprecated in Servlet API version 2.4.

  注意:SingleThreadModel不會解決所有的線程安全隱患。 例如,會話屬性和靜態變量仍然可以被多線程的多請求同時訪問,即便使用了SingleThreadModel servlet。建議開發人員應當采取其他手段來解決這些問題,而不是實現該接口,比如 避免實例變量的使用或者在訪問資源時同步代碼塊。該接口在Servlet API 2.4中將不推薦使用。

   2、同步對共享數據的操作

  使用synchronized 關鍵字能保證一次只有一個線程可以訪問被保護的區段,在本論文中的Servlet可以通過同步塊操作來保證線程的安全。同步后的代碼如下:

 1 public class ConcurrentTest extends HttpServlet {
 2     PrintWriter output;
 3     @Override
 4     protected void service(HttpServletRequest request, HttpServletResponse response)
 5             throws ServletException, IOException {
 6         String  username;
 7         response.setContentType("text/html;charset=gb2312");
 8         username=request.getParameter("username");
 9         synchronized(this){
10             output=response.getWriter();
11             try {
12                 //為了突出並發問題,在這設置一個延時
13                 Thread.sleep(5000);
14                 output.println("用戶名:"+username+"<BR>"); 
15             } catch (Exception e) {
16                 e.printStackTrace();
17             }
18         }
19     }
20 }

  

  3、避免使用實例變量

  本實例中的線程安全問題是由實例變量造成的,只要在Servlet里面的任何方法里面都不使用實例變量,那么該Servlet就是線程安全的。

  修正上面的Servlet代碼,將實例變量改為局部變量實現同樣的功能,代碼如下:

 1 public class ConcurrentTest extends HttpServlet {
 2     @Override
 3     protected void service(HttpServletRequest request, HttpServletResponse response)
 4             throws ServletException, IOException {
 5         PrintWriter output;
 6         String username;
 7         response.setContentType("text/html;charset=gb2312");
 8         username=request.getParameter("username");
 9         synchronized(this){
10             output=response.getWriter();
11             try {
12                 //為了突出並發問題,在這設置一個延時
13                 Thread.sleep(5000);
14                 output.println("用戶名:"+username+"<BR>"); 
15             } catch (Exception e) {
16                 e.printStackTrace();
17             }
18         }
19     }
20 }

  對上面的三種方法進行測試,可以表明用它們都能設計出線程安全的Servlet程序。但是,如果一個Servlet實現了SingleThreadModel接口,Servlet引擎將為每個新的請求創建一個單獨的Servlet實例,這將引起大量的系統開銷。SingleThreadModel在Servlet2.4中已不再提倡使用;同樣如果在程序中使用同步來保護要使用的共享的數據,也會使系統的性能大大下降。這是因為被同步的代碼塊在同一時刻只能有一個線程執行它,使得其同時處理客戶請求的吞吐量降低,而且很多客戶處於阻塞狀態。另外為保證主存內容和線程的工作內存中的數據的一致性,要頻繁地刷新緩存,這也會大大地影響系統的性能。所以在實際的開發中也應避免或最小化 Servlet 中的同步代碼;在Serlet中避免使用實例變量是保證Servlet線程安全的最佳選擇。從Java 內存模型也可以知道,方法中的臨時變量是在棧上分配空間,而且每個線程都有自己私有的棧空間,所以它們不會影響線程的安全。

  小結

  Servlet的線程安全問題只有在大量的並發訪問時才會顯現出來,並且很難發現,因此在編寫Servlet程序時要特別注意。線程安全問題主要是由實例變量造成的,因此在Servlet中應避免使用實例變量。如果應用程序設計無法避免使用實例變量,那么使用同步來保護要使用的實例變量,但為保證系統的最佳性能,應該同步可用性最小的代碼路徑。

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM