Servlet的線程安全

jopen 10年前發布 | 24K 次閱讀 Servlet Java開發

Servlet的多線程機制

    

1.  變量的線性安全:這里的變量指字段和共享數據(如表單參數值)。

  • 將參數變量本地化。多線程并不共享局部變量,所以要盡可能地在servlet中使用局部變量。例如:String user=request.getParameter("user");
  • 使用同步塊Synchronized,防止可能異步調用的代碼塊,這就意味著線程需要排隊處理。但要注意在使用同步塊的范圍要盡可能的小,不要直接在sevice方法和響應方法上使用,這樣會嚴重影響性能。

2.  屬性的線性安全:ServletContext,HttpSession,ServletRequest對象中的屬性。

  • ServletContext(線程不安全):ServletContext可以同時進行多線程讀/寫屬性,線程是不安全的。要對屬性的讀寫進行 同步處理或進行深度Clone()。所以在Servlet上下文中要盡量少保存會被修改(寫)的數據,可以使用其他的方式在多個Servlet中共享,比 如使用單例模式處理共享數據。
  • HttpSession(線程不安全):HttpSession在用戶會話期間存在,只能在處理屬于同一 個Session請求的線程中被訪問,因此理論上訪問Session對象的屬性是線程安全的。但是當用戶打開同屬于同一個進程的瀏覽窗口,對這些窗口的訪 問屬于同一個session,會出現多次請求,需要多個工作線程來處理,可能會造成多個線程同時讀寫操作。這時我們就需要對屬性的讀寫進行同步處理:使用 同步塊或讀/寫器來處理。
  • ServletRequest(線程安全):對于每一個請求,由一個線程來執行,都會創建一個新的 ServletRequest對象,所以ServletRequest只能在一個線程中被訪問。注意:ServletRequest對象在service 方法的范圍內是有效的,不要試圖在service方法結束后仍然保存訪問請求對象的引用。

3.  不要在Servlet中創建自己的線程以完成某個功能:servlet本身就是多線程,再創建線程會導致問題復雜化,會帶來線程安全的問題。

 

4.  在多個Servlet中對外部對象(比如文件)修改一定要加鎖,做到互斥的訪問。

 

5.  javax.servlet.SingleThreadModel接口是一個標識接口,如果一個servlet實現了這個接口,那么servlet容器將 保證在一個時刻僅有一個線程可以在給定的servlet實例的service方法中執行,將其他所有請求進行排隊。

 

6.  服務器可以使用多個實例來處理請求,代替單個實例的請求排隊帶來的效益問題。服務器創建一個Servlet類的多個Servlet實例組成的實例池,對于 每個請求分配Servlet實例進行響應處理,之后放回到實例池中等待下此請求。這樣就造成并發訪問的問題。此時,局部變量(字段)也是安全的,但對于全 局變量和共享數據是不安全的,需要進行同步處理。而對于這樣多實例的情況SingleThreadModel接口并不能解決并發訪問問題。

</div>

 

 

        Servlet體系結構是建立在Java多線程機制之上的,它的生命周期是由Web容器負責的。

當客戶端第一次請求某個Servlet時,Servlet容器將會根據web.xml配置文件實例 化這個Servlet類。當有新的客戶端請求該Servlet時,一般不會再實例化該Servlet類,也就是有多個線程在使用這個實例。Servlet 容器會自動使用線程池等技術來支持系統的運行,如圖1所示。


Servlet的線程安全
圖1 Servlet線程池

        這樣,當兩個或多個線程同時訪問同一個Servlet時,可能會發生多個線程同時訪問同一資源的情況,數據可能會變得不一致。所以在用Servlet構建的Web應用時如果不注意線程安全的問題,會使所寫的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");  
            PrintWriter output = response.getWriter ();  
            try{  
                Thread. sleep (5000); //為了突出并發問題,在這設置一個延時  
            } catch (Interrupted Exception e){}  

            output.println("用戶名:"+Username+"");  
        }  
    }  

</div>

    </div>



            該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所示的輸出:


    Servlet的線程安全
    圖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),工作內存由緩存和堆棧兩部分組成,緩存中保存的是主存中變量的拷貝,緩存可能并不總和主存同步,也就是緩存中變量的修改可能沒有立刻寫到主存中;堆棧中保存的是線程的局部變量(save in stack, not in heap),線程之間無法相互直接訪問堆棧中的變量。根據JMM,我們可以將論文中所討論的Servlet實例的內存模型抽象為圖3所示的模型。</span>


    Servlet的線程安全
    圖3 Servlet實例的JMM模型

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


     <td width="38%">a線程</td>
    
     <td width="48%">b線程</td>
    
    </tr>
    
    <tr bgcolor="#ffffff"> 
     <td width="14%">T1</td>
    
     <td width="38%">訪問Servlet頁面</td>
    
     <td width="48%"> </td>
    
    </tr>
    
    <tr bgcolor="#ffffff"> 
     <td width="14%">T2</td>
    
     <td width="38%"> </td>
    
     <td width="48%">訪問Servlet頁面</td>
    
    </tr>
    
    <tr bgcolor="#ffffff"> 
     <td width="14%">T3</td>
    
     <td width="38%">output=a的輸出username=a休眠5000毫秒,讓出CPU</td>
    
     <td width="48%"> </td>
    
    </tr>
    
    <tr bgcolor="#ffffff"> 
     <td width="14%">T4</td>
    
     <td width="38%"> </td>
    
     <td width="48%">output=b的輸出(寫回主存)username=b休眠5000毫秒,讓出CPU</td>
    
    </tr>
    
    <tr bgcolor="#ffffff"> 
     <td width="14%">T5</td>
    
     <td width="38%">在用戶b的瀏覽器上輸出a線程的username的值,a線程終止。</td>
    
     <td width="48%"> </td>
    
    </tr>
    
    <tr bgcolor="#ffffff"> 
     <td width="14%">T6</td>
    
     <td width="38%"> </td>
    
     <td width="48%">在用戶b的瀏覽器上輸出b線程的username的值,b線程終止。</td>
    
    </tr>
    
    

    </tbody>

    </table>                                                                         圖4 Servlet實例的線程調度情況


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



    設計線程安全的Servlet

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

            1、實現 SingleThreadModel 接口

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

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

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

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

    …………
    </span>

    </div>

    </div>

        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+"  
        ");  
        }  
        }  
        }  


            3、避免使用實例變量


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


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


    ……

    </div>

    </div>

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

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


             小結


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

     本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
     轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
     本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
    調度時刻
  1. sesese色