深入研究Servlet線(xiàn)程安全性問(wèn)題
2005-05-23 09:18 作者: 雷軍環(huán) 出處: 計算機與信息技術(shù) 責任編輯:方舟
摘 要:介紹了Servlet多線(xiàn)程機制,通過(guò)一個(gè)實(shí)例并結合Java 的
內存模型說(shuō)明引起Servlet線(xiàn)程不安全的原因,給出了保證Servlet線(xiàn)程安全的三種解決方案,并說(shuō)明三種方案在實(shí)際開(kāi)發(fā)中的取舍。
關(guān)鍵字:Servlet 線(xiàn)程安全 同步 Java內存模型 實(shí)例變量
Servlet/JSP技術(shù)和ASP、PHP等相比,由于其多線(xiàn)程運行而具有很高的執行效率。由于Servlet/JSP
默認是以多線(xiàn)程模式執行的,所以,在編寫(xiě)代碼時(shí)需要非常細致地考慮多線(xiàn)程的安全性問(wèn)題。然而,很多人編寫(xiě)Servlet/JSP
程序時(shí)并沒(méi)有注意到多線(xiàn)程安全性的問(wèn)題,這往往造成編寫(xiě)的程序在少量用戶(hù)訪(fǎng)問(wèn)時(shí)沒(méi)有任何問(wèn)題,而在
并發(fā)用戶(hù)上升到一定值時(shí),就會(huì )經(jīng)常出現一些莫明其妙的問(wèn)題。
Servlet的多線(xiàn)程機制 Servlet體系結構是建立在Java多線(xiàn)程機制之上的,它的生命周期是由Web容器負責的?!?】當客戶(hù)端第一次請求某個(gè)Servlet時(shí),Servlet容器將會(huì )根據web.xml配置文件
實(shí)例化這個(gè)Servlet類(lèi)?!?】當有新的客戶(hù)端請求該Servlet時(shí),一般不會(huì )再實(shí)例化該Servlet類(lèi),也就是有多個(gè)線(xiàn)程在
使用這個(gè)實(shí)例。Servlet容器會(huì )自動(dòng)使用線(xiàn)程池等技術(shù)來(lái)支持系統的運行,如圖1所示。
這樣,當兩個(gè)或多個(gè)線(xiàn)程【一個(gè)用戶(hù)對應一個(gè)線(xiàn)程】同時(shí)訪(fǎng)問(wèn)同一個(gè)Servlet時(shí),可能會(huì )發(fā)生多個(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn)同一資源的情況,數據可能會(huì )變得不一致。所以在用Servlet構建的Web應用時(shí)如果不注意線(xiàn)程安全的問(wèn)題,會(huì )使所寫(xiě)的Servlet程序有難以發(fā)現的錯誤。
Servlet的線(xiàn)程安全問(wèn)題
Servlet的線(xiàn)程安全問(wèn)題主要是由于實(shí)例變量使用不當而引起的,這里以一個(gè)現實(shí)的例子來(lái)說(shuō)明。
| 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); //為了突出并發(fā)問(wèn)題,在這設置一個(gè)延時(shí) } Catch (Interrupted Exception e){} output.println("用戶(hù)名:"+Username+"<BR>"); } } |
該Servlet中定義了一個(gè)實(shí)例變量output,在service方法將其賦值為用戶(hù)的輸出。當一個(gè)用戶(hù)訪(fǎng)問(wèn)該Servlet時(shí),程序會(huì )正常的運行,但當多個(gè)用戶(hù)并發(fā)訪(fǎng)問(wèn)時(shí),就可能會(huì )出現其它用戶(hù)的信息顯示在另外一些用戶(hù)的瀏覽器上的問(wèn)題。這是一個(gè)嚴重的問(wèn)題。為了突出并發(fā)問(wèn)題,便于測試、觀(guān)察,我們在回顯用戶(hù)信息時(shí)執行了一個(gè)延時(shí)的操作。假設已在web.xml配置文件中注冊了該Servlet,現有兩個(gè)用戶(hù)a和b同時(shí)訪(fǎng)問(wèn)該Servlet(可以啟動(dòng)兩個(gè)IE瀏覽器,或者在兩臺機器上同時(shí)訪(fǎng)問(wèn)),即同時(shí)在瀏覽器中輸入:
a: http://localhost: 8080/servlet/ConcurrentTest? Username=a
b: http://localhost: 8080/servlet/ConcurrentTest? Username=b
如果用戶(hù)b比用戶(hù)a回車(chē)的時(shí)間稍慢一點(diǎn),將得到如圖2所示的輸出:
從圖2中可以看到,Web
服務(wù)器啟動(dòng)了兩個(gè)線(xiàn)程分別處理來(lái)自用戶(hù)a和用戶(hù)b的請求,但是在用戶(hù)a的瀏覽器上卻得到一個(gè)空白的屏幕,用戶(hù)a的信息顯示在用戶(hù)b的瀏覽器上。該Servlet存在線(xiàn)程不安全問(wèn)題。下面我們就從分析該實(shí)例的內存模型入手,觀(guān)察不同時(shí)刻實(shí)例變量output的值來(lái)分析使該Servlet線(xiàn)程不安全的原因。
Java的內存模型JMM(Java Memory Model)JMM主要是為了規定了線(xiàn)程和內存之間的一些關(guān)系。根據JMM的
設計,系統存在一個(gè)
主內存(Main Memory),Java中所有實(shí)例變量都儲存在主存中,對于所有線(xiàn)程都是共享的。每條線(xiàn)程都有自己的
工作內存(Working Memory),工作內存由
緩存和
堆棧兩部分組成,
緩存中保存的是主存中變量的拷貝,
緩存可能并不總和主存同步,也就是緩存中變量的修改
可能沒(méi)有立刻寫(xiě)到主存中;堆棧中保存的是
線(xiàn)程的局部變量,線(xiàn)程之間無(wú)法相互直接訪(fǎng)問(wèn)堆棧中的變量。根據JMM,我們可以將論文中所討論的Servlet實(shí)例的內存模型抽象為圖3所示的模型。
下面根據圖3所示的內存模型,來(lái)分析當用戶(hù)a和b的線(xiàn)程(簡(jiǎn)稱(chēng)為a線(xiàn)程、b線(xiàn)程)并發(fā)執行時(shí),Servlet實(shí)例中所涉及變量的變化情況及線(xiàn)程的執行情況,如圖4所示。
| 調度時(shí)刻 | a線(xiàn)程 | b線(xiàn)程 |
| T1 | 訪(fǎng)問(wèn)Servlet頁(yè)面 | |
| T2 | | 訪(fǎng)問(wèn)Servlet頁(yè)面 |
| T3 | output=a的輸出username=a休眠5000毫秒,讓出CPU | |
| T4 | | output=b的輸出(寫(xiě)回主存)username=b休眠5000毫秒,讓出CPU |
| T5 | 在用戶(hù)b的瀏覽器上輸出a線(xiàn)程的username的值,a線(xiàn)程終止。 | |
| T6 | | 在用戶(hù)b的瀏覽器上輸出b線(xiàn)程的username的值,b線(xiàn)程終止。 |
圖4 Servlet實(shí)例的線(xiàn)程調度情況
從圖4中可以清楚的看到,由于b線(xiàn)程對實(shí)例變量output的修改覆蓋了a線(xiàn)程對實(shí)例變量output的修改,從而導致了用戶(hù)a的信息顯示在了用戶(hù)b的瀏覽器上。如果在a線(xiàn)程執行輸出語(yǔ)句時(shí),b線(xiàn)程對output的修改還沒(méi)有刷新到主存,那么將不會(huì )出現圖2所示的輸出結果,因此這只是一種偶然現象,但這更增加了程序潛在的危險性。
設計線(xiàn)程安全的Servlet
通過(guò)上面的分析,我們知道了實(shí)例變量不正確的使用是造成Servlet線(xiàn)程不安全的主要原因。下面針對該問(wèn)題給出了三種解決方案并對方案的選取給出了一些參考性的建議。
1、實(shí)現
SingleThreadModel 接口
該接口指定了系統如何處理對同一個(gè)Servlet的調用。如果一個(gè)Servlet被這個(gè)接口指定,那么在這個(gè)Servlet中的service方法將不會(huì )有兩個(gè)線(xiàn)程被同時(shí)執行,當然也就不存在線(xiàn)程安全的問(wèn)題。這種方法只要將前面的Concurrent Test類(lèi)的類(lèi)頭定義更改為:
Public class Concurrent Test extends HttpServlet implements SingleThreadModel { ………… } |
2、同步對共享數據的操作
使用synchronized 關(guān)鍵字能保證一次只有一個(gè)線(xiàn)程可以訪(fǎng)問(wèn)被保護的區段,在本論文中的Servlet可以通過(guò)同步塊操作來(lái)保證線(xiàn)程的安全。同步后的代碼如下:
………… 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("用戶(hù)名:"+Username+"<BR>"); } } } |
3、避免使用實(shí)例變量
本實(shí)例中的線(xiàn)程安全問(wèn)題是由實(shí)例變量造成的,只要在Servlet里面的任何方法里面都不使用實(shí)例變量,那么該Servlet就是線(xiàn)程安全的。
修正上面的Servlet代碼,將實(shí)例變量改為局部變量實(shí)現同樣的功能,代碼如下:
…… 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"); …… } } |
對上面的三種方法進(jìn)行測試,可以表明用它們都能設計出線(xiàn)程安全的Servlet程序。但是,如果一個(gè)Servlet實(shí)現了SingleThreadModel接口,Servlet引
擎將為每個(gè)新的請求創(chuàng )建一個(gè)單獨的Servlet實(shí)例,這將引起大量的系統開(kāi)銷(xiāo)。SingleThreadModel在Servlet2
.4中已不再提倡使用;同樣如果在程序中使用同步來(lái)保護要使用的共享的數據,也會(huì )使系統的性能大大下降。這是因為被同步的代碼塊在同一時(shí)刻只能有一個(gè)線(xiàn)程執行它,使得其同時(shí)處理客戶(hù)請求的吞吐量降低,而且很多客戶(hù)處于阻塞狀態(tài)。另外為保證主存內容和線(xiàn)程的工作內存中的數據的一致性,要頻繁地刷新緩存,這也會(huì )大大地影響系統的性能。所以在實(shí)際的開(kāi)發(fā)中也應避免或最小化 Servlet 中的同步代碼;在Serlet中避免使用實(shí)例變量是保證Servlet線(xiàn)程安全的最佳選擇。從Java 內存模型也可以知道,方法中的臨時(shí)變量是在棧上分配空間,而且每個(gè)線(xiàn)程都有自己私有的??臻g,所以它們不會(huì )影響線(xiàn)程的安全。
小結 Servlet的線(xiàn)程安全問(wèn)題只有在
大量的并發(fā)訪(fǎng)問(wèn)時(shí)才會(huì )顯現出來(lái),并且很難發(fā)現,因此在編寫(xiě)Servlet程序時(shí)要特別注意。線(xiàn)程安全問(wèn)題主要是由實(shí)例變量造成的,因此在Servlet中應避免使用實(shí)例變量。如果應用程序設計無(wú)法避免使用實(shí)例變量,那么使用同步來(lái)保護要使用的實(shí)例變量,但為保證系統的最佳性能,應該同步可用性最小的代碼路徑。