1. 程式人生 > >Servlet的多線程和線程安全

Servlet的多線程和線程安全

request對象 封裝 到來 只有一個 將不 包含 卸載 end servlet實例

線程安全

首先說明一下對線程安全的討論,哪種情況我們可以稱作線程安全?
網上對線程安全有很多描述,我比較喜歡《Java並發編程實戰》給出的定義,“當多個線程訪問某個類時,不管運行時環境采用何種調度方式,或者這些線程將如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那麽就稱這個類是線程安全的”。

Servlet的調用過程和生命周期

Servlet的生命周期

Servlet是運行在Servlet容器中的,常用的tomcat、jboss、weblogic都是Servlet容器,其生命周期是由容器來管理。Servlet的生命周期通過java.servlet.Servlet接口中的init()、service()、和destroy()方法表示。Servlet的生命周期有四個階段:加載並實例化、初始化、請求處理、銷毀

加載並實例化
Servlet容器負責加載和實例化Servelt。當Servlet容器啟動時,或者在容器檢測到需要這個Servlet來響應第一個請求時,創建Servlet實例。當Servlet容器啟動後,Servlet通過類加載器來加載Servlet類,加載完成後再new一個Servlet對象來完成實例化。

初始化
在Servlet實例化之後,容器將調用init()方法,並傳遞實現ServletConfig接口的對象。在init()方法中,Servlet可以部署描述符中讀取配置參數,或者執行任何其他一次性活動。在Servlet的整個生命周期類,init()方法只被調用一次。

請求處理
當Servlet初始化後,容器就可以準備處理客戶機請求了。當容器收到對這一Servlet的請求,就調用Servlet的service()方法,並把請求和響應對象作為參數傳遞。當並行的請求到來時,多個service()方法能夠同時運行在獨立的線程中。通過分析ServletRequest或者HttpServletRequest對象,service()方法處理用戶的請求,並調用ServletResponse或者HttpServletResponse對象來響應。

銷毀
一旦Servlet容器檢測到一個Servlet要被卸載,這可能是因為要回收資源或者因為它正在被關閉,容器會在所有Servlet的service()線程之後,調用Servlet的destroy()方法。然後,Servlet就可以進行無用存儲單元收集清理。這樣Servlet對象就被銷毀了。這四個階段共同決定了Servlet的生命周期。

Servlet的調用過程

1.客戶端通過發送請求給Tomcat,Tomcat發送客戶端的請求頁面給客戶端,這是一個靜態頁面。

2.用戶對請求頁面進行相關操作後將頁面提交給Tomcat,Tomcat將其封裝成一個HttpRequest對象,然後對請求進行處理,。

3.Tomcat截獲請求,根據action屬性值查詢xml文件中對應的servlet-name,再根據servlet-name查詢到對應的java類(如果是第一次,Tomcat則會將servlet編譯成java類文件,所以如果servlet有很多的話第一次運行的時候程序會比較慢)。

4.Tomcat實例化查詢到的java類,註意該類只實例化一次

5.調用java類對象的service()方法(如果不對service()方法進行重寫則根據提交的方式來決定執行doPost()方法還是doGet()方法)。

6.通過request對象取得客戶端傳過來的數據,對數據進行處理後通過response對象將處理結果寫回客戶端。

Servlet是線程安全的嗎?

從上面Servlet的調用過程可以看出,當客戶端第一次請求Servlet的時候,tomcat會根據web.xml配置文件實例化servlet,

當又有一個客戶端訪問該servlet的時候,不會再實例化該servlet,也就是多個線程在使用這個實例。

JSP/Servlet容器默認是采用單實例多線程(這是造成線程安全的主因)方式處理多個請求的,這種默認以多線程方式執行的設計可大大降低對系統的資源需求,提高系統的並發量及響應時間。

Servlet本身是無狀態的,一個無狀態的Servlet是絕對線程安全的,無狀態對象設計也是解決線程安全問題的一種有效手段。

所以,servlet是否線程安全是由它的實現來決定的,如果它內部的屬性或方法會被多個線程改變,它就是線程不安全的,反之,就是線程安全的。

下面這個示例來自《Java並發編程實戰》,在競態條件下存在線程不安全。

public class UnsafeCountingFactorizer implements Servlet{
 
private long count=0;
public long getCount(){
return count;
}
 
@Override
public void service(ServletRequest req, ServletResponse resp)
throws ServletException, IOException {
++count;
 
}
}

遞增操作count++並非是原子操作,它包含了三個獨立的操作:讀取count的值,將值加1,
然後將計算結果寫入coune,這是一個“讀取-修改-寫入”的操作序列,並且其結果狀態依賴於之前的狀態。在執行時序不同的情況下,可能會產生錯誤

影響Servlet線程安全的因素

多線程下每個線程對局部變量都會有自己的一份copy,這樣對局部變量的修改只會影響到自己的copy而不會對別的線程產生影響,所以這是線程安全的。

但是對於實例變量來說,由於servlet在Tomcat中是以單例模式存在的,所有的線程共享實例變量。多個線程對共享資源的訪問就造成了線程不安全問題

如何控制Servlet的線程安全性?

避免使用實例變量
避免使用非線程安全的集合
在多個Servlet中對某個外部對象(例如文件)的修改是務必加鎖(Synchronized,或者ReentrantLock),互斥訪問。
屬性的線程安全:ServletContext、HttpSession是線程安全的;ServletRequest是非線程安全的。

設計線程安全的Servlet

1.實現 SingleThreadModel 接口
該接口指定了系統如何處理對同一個Servlet的調用。如果一個Servlet被這個接口指定,那麽在這個Servlet中的service方法將不會有兩個線程被同時執行,當然也就不存在線程安全的問題。但是,如果一個Servlet實現了SingleThreadModel接口,Servlet引擎將為每個新的請求創建一個單獨的Servlet實例,這將引起大量的系統開銷,在現在的Servlet開發中基本看不到SingleThreadModel的使用,這種方式了解即可,盡量避免使用。

public class XXXXX extends HttpServlet implements SingleThreadModel {
…………
}

2.同步對共享數據的操作
使用synchronized 關鍵字能保證一次只有一個線程可以訪問被保護的區段,可以通過同步塊操作來保證Servlet的線程安全。如果在程序中使用同步來保護要使用的共享的數據,也會使系統的性能大大下降。這是因為被同步的代碼塊在同一時刻只能有一個線程執行它,使得其同時處理客戶請求的吞吐量降低,而且很多客戶處於阻塞狀態。另外為保證主存內容和線程的工作內存中的數據的一致性,要頻繁地刷新緩存,這也會大大地影響系統的性能。所以在實際的開發中也應避免或最小化Servlet 中的同步代碼。

同步代碼:

Public class XXXXXX extends HttpServlet {
synchronized (this){XXXX}
}

3.避免使用實例變量
線程安全問題很大部分是由實例變量造成的,只要在Servlet裏面的任何方法裏面都不使用實例變量,那麽該Servlet就是線程安全的。

在Servlet中避免使用實例變量是保證Servlet線程安全的最佳選擇。

Java 內存模型中,方法中的臨時變量是在棧上分配空間,而且每個線程都有自己私有的棧空間,所以它們不會影響線程的安全。

Servlet的多線程和線程安全