1. 程式人生 > >servlet單例項多執行緒模式

servlet單例項多執行緒模式

Servlet/JSP技術和ASP、PHP等相比,由於其多執行緒執行而具有很高的執行效率。由於Servlet/JSP預設是以多執行緒模式執行的,所以,在編寫程式碼時需要非常細緻地考慮多執行緒的安全性問題。

 
JSP的中存在的多執行緒問題: 
當客戶端第一次請求某一個JSP檔案時,服務端把該JSP編譯成一個CLASS檔案,並建立一個該類的例項,然後建立一個執行緒處理CLIENT端的請求。如果有多個客戶端同時請求該JSP檔案,則服務端會建立多個執行緒。每個客戶端請求對應一個執行緒。以多執行緒方式執行可大大降低對系統的資源需求,提高系統的併發量及響應時間.

對JSP中可能用的的變數說明如下: 
例項變數: 例項變數是在堆中分配的,並被屬於該例項的所有執行緒共享,所以不是執行緒安全的. 
JSP系統提供的8個類變數 
JSP中用到的OUT,REQUEST,RESPONSE,SESSION,CONFIG,PAGE,PAGECONXT是執行緒安全的(因為每個執行緒對應的request,respone物件都是不一樣的,不存在共享問題),

 APPLICATION在整個系統內被使用,所以不是執行緒安全的.

區域性變數: 區域性變數在堆疊中分配,因為每個執行緒都有它自己的堆疊空間,所以是執行緒安全的. 
靜態類: 靜態類不用被例項化,就可直接使用,也不是執行緒安全的. 

外部資源: 在程式中可能會有多個執行緒或程序同時操作同一個資源(如:多個執行緒或程序同時對一個檔案進行寫操作).此時也要注意同步問題. 

使它以單執行緒方式執行,這時,仍然只有一個例項,所有客戶端的請求以序列方式執行。這樣會降低系統的效能 
     
問題 
問題一.
 說明其Servlet容器如何採用單例項多執行緒的方式來處理請求 
問題二. 如何在開發中保證servlet是單例項多執行緒的方式來工作(也就是說如何開發執行緒安全的servelt)。 
     
一. Servlet容器如何同時來處理多個請求 
    

Java的記憶體模型JMM(Java Memory Model) 
JMM主要是為了規定了執行緒和記憶體之間的一些關係。根據JMM的設計,系統存在一個主記憶體(Main Memory),Java中所有例項變數都儲存在主存中,對於所有執行緒都是共享的。每條執行緒都有自己的工作記憶體(Working Memory),工作記憶體由快取和堆疊兩部分組成,快取中儲存的是主存中變數的拷貝,快取可能並不總和主存同步,也就是快取中變數的修改可能沒有立刻寫到主存中;堆疊中儲存的是執行緒的區域性變數,執行緒之間無法相互直接訪問堆疊中的變數。根據JMM,我們可以將論文中所討論的Servlet例項的記憶體模型抽象為圖所示的模型。



         工作者執行緒Work Thread:執行程式碼的一組執行緒。 
         排程執行緒Dispatcher Thread:每個執行緒都具有分配給它的執行緒優先順序,執行緒是根據優先順序排程執行的。 
        
        Servlet採用多執行緒來處理多個請求同時訪問。servlet依賴於一個執行緒池來服務請求。執行緒池實際上是一系列的工作者執行緒集合。Servlet使用一個排程執行緒來管理工作者執行緒。
       
        當容器收到一個Servlet請求,排程執行緒從執行緒池中選出一個工作者執行緒,將請求傳遞給該工作者執行緒,然後由該執行緒來執行Servlet的service方法。當這個執行緒正在執行的時候,容器收到另外一個請求,排程執行緒同樣從執行緒池中選出另一個工作者執行緒來服務新的請求,容器並不關心這個請求是否訪問的是同一個Servlet.當容器同時收到對同一個Servlet的多個請求的時候,那麼這個Servlet的service()方法將在多執行緒中併發執行。
         Servlet容器預設採用單例項多執行緒的方式來處理請求,這樣減少產生Servlet例項的開銷,提升了對請求的響應時間,對於Tomcat可以在server.xml中通過<Connector>元素設定執行緒池中執行緒的數目。
       
        就實現來說: 
        排程者執行緒類所擔負的責任如其名字,該類的責任是排程執行緒,只需要利用自己的屬性完成自己的責任。所以該類是承擔了責任的,並且該類的責任又集中到唯一的單體物件中。而其他物件又依賴於該特定物件所承擔的責任,我們就需要得到該特定物件。那該類就是一個單例模式的實現了。 
    

注意:伺服器可以使用多個例項來處理請求,代替單個例項的請求排隊帶來的效益問題。伺服器建立一個Servlet類的多個Servlet例項組成的例項池,對於每個請求分配Servlet例項進行響應處理,之後放回到例項池中等待下此請求。這樣就造成併發訪問的問題。
此時,區域性變數(欄位)也是安全的,但對於全域性變數和共享資料是不安全的,需要進行同步處理。而對於這樣多例項的情況SingleThreadModel介面並不能解決併發訪問問題。 SingleThreadModel介面在servlet規範中已經被廢棄了。


二 如何開發執行緒安全的Servlet

  1、實現 SingleThreadModel 介面 

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

Public class Concurrent Test extends HttpServlet implements SingleThreadModel { 
………… 
}  

  2、同步對共享資料的操作 

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

………… 
Public class Concurrent Test extends HttpServlet { ………… 
Username = request.getParameter ("username"); 
Synchronized (this){
Output = response.getWriter (); 
Try { 
Thread. Sleep (5000); 
} Catch (Interrupted Exception e){} 
output.println("使用者名稱:"+Username+"<BR>"); 




  3、避免使用例項變數 

  本例項中的執行緒安全問題是由例項變數造成的,只要在Servlet裡面的任何方法裡面都不使用例項變數,那麼該Servlet就是執行緒安全的。 

  修正上面的Servlet程式碼,將例項變數改為區域性變數實現同樣的功能,程式碼如下: 

…… 
Public class Concurrent Test extends HttpServlet {public void service (HttpServletRequest request, HttpServletResponse
Response) throws ServletException, IOException { 
Print Writer output; 
String username; 
Response.setContentType ("text/html; charset=gb2312"); 
…… 




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

更加詳細的說明:


   1,變數的執行緒安全:這裡的變數指欄位和共享資料(如表單引數值)。 
       a,將 引數變數 本地化。多執行緒並不共享區域性變數.所以我們要儘可能的在servlet中使用區域性變數。 
          例如:String user = ""; 
              user = request.getParameter("user"); 

      b,使用同步塊Synchronized,防止可能非同步呼叫的程式碼塊。這意味著執行緒需要排隊處理。在使用同板塊的時候要儘可能的縮小同步程式碼的範圍,不要直接在sevice方法和響應方法上使用同步,這樣會嚴重影響效能。

       2,屬性的執行緒安全:ServletContext,HttpSession,ServletRequest物件中屬性。 
         ServletContext:(執行緒是不安全的) 
         ServletContext是可以多執行緒同時讀/寫屬性的,執行緒是不安全的。要對屬性的讀寫進行同步處理或者進行深度Clone()。所以在Servlet上下文中儘可能少量儲存會被修改(寫)的資料,可以採取其他方式在多個Servlet中共享,比方我們可以使用單例模式來處理共享資料。
         HttpSession:(執行緒是不安全的) 
         HttpSession物件在使用者會話期間存在,只能在處理屬於同一個Session的請求的執行緒中被訪問,因此Session物件的屬性訪問理論上是執行緒安全的。
         當用戶開啟多個同屬於一個程序的瀏覽器視窗,在這些視窗的訪問屬於同一個Session,會出現多次請求,需要多個工作執行緒來處理請求,可能造成同時多執行緒讀寫屬性。這時我們需要對屬性的讀寫進行同步處理:使用同步塊Synchronized和使用讀/寫器來解決。
        ServletRequest:(執行緒是安全的) 
        對於每一個請求,由一個工作執行緒來執行,都會建立有一個新的ServletRequest物件,所以ServletRequest物件只能在一個執行緒中被訪問。ServletRequest是執行緒安全的。注意:ServletRequest物件在service方法的範圍內是有效的,不要試圖在service方法結束後仍然儲存請求物件的引用。

3,使用同步的集合類: 
  使用Vector代替ArrayList,使用Hashtable代替HashMap。 

4,不要在Servlet中建立自己的執行緒來完成某個功能。 
  Servlet本身就是多執行緒的,在Servlet中再建立執行緒,將導致執行情況複雜化,出現多執行緒安全問題。 

5,在多個servlet中對外部物件(比方檔案)進行修改操作一定要加鎖,做到互斥的訪問。 
   
6,javax.servlet.SingleThreadModel介面是一個標識介面,如果一個Servlet實現了這個介面,那Servlet容器將保證在一個時刻僅有一個執行緒可以在給定的servlet例項的service方法中執行。將其他所有請求進行排隊。



PS:

Servlet並非只是單例的. 當container開始啟動,或是客戶端發出請求服務時,Container會按照容器的配置負責載入和例項化一個Servlet(也可以配置為多個,不過一般不這麼幹).不過一般來說一個servlet只會有一個例項。

1) Struts2的Action是原型,非單例項的;會對每一個請求,產生一個Action的例項來處理。 
2) Struts1的Action,Spring的Ioc容器管理的bean 預設是單例項的.


Struts1 Action是單例項的,spring mvc的controller也是如此。因此開發時要求必須是執行緒安全的,因為僅有Action的一個例項來處理所有的請求。單例策略限制了Struts1 Action能作的事,並且要在開發時特別小心。Action資源必須是執行緒安全的或同步的。

Spring的Ioc容器管理的bean 預設是單例項的。

Struts2 Action物件為每一個請求產生一個例項,因此沒有執行緒安全問題。(實際上,servlet容器給每個請求產生許多可丟棄的物件,並且不會導致效能和垃圾回收問題)。

當Spring管理Struts2的Action時,bean預設是單例項的,可以通過配置引數將其設定為原型。(scope="prototype )