首先說明一下對線程安全的討論,哪一種狀況咱們能夠稱做線程安全?
網上對線程安全有不少描述,我比較喜歡《Java併發編程實戰》給出的定義,「當多個線程訪問某個類時,無論運行時環境採用何種調度方式,或者這些線程將如何交替執行,而且在主調代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的」。html
Servlet是運行在Servlet容器中的,經常使用的tomcat、jboss、weblogic都是Servlet容器,其生命週期是由容器來管理。Servlet的生命週期經過java.servlet.Servlet接口中的init()、service()、和destroy()方法表示。Servlet的生命週期有四個階段:加載並實例化、初始化、請求處理、銷燬。
加載並實例化
Servlet容器負責加載和實例化Servelt。當Servlet容器啓動時,或者在容器檢測到須要這個Servlet來響應第一個請求時,建立Servlet實例。當Servlet容器啓動後,Servlet經過類加載器來加載Servlet類,加載完成後再new一個Servlet對象來完成實例化。java
初始化
在Servlet實例化以後,容器將調用init()方法,並傳遞實現ServletConfig接口的對象。在init()方法中,Servlet能夠部署描述符中讀取配置參數,或者執行任何其餘一次性活動。在Servlet的整個生命週期類,init()方法只被調用一次。web
請求處理
當Servlet初始化後,容器就能夠準備處理客戶機請求了。當容器收到對這一Servlet的請求,就調用Servlet的service()方法,並把請求和響應對象做爲參數傳遞。當並行的請求到來時,多個service()方法可以同時運行在獨立的線程中。經過分析ServletRequest或者HttpServletRequest對象,service()方法處理用戶的請求,並調用ServletResponse或者HttpServletResponse對象來響應。編程
銷燬
一旦Servlet容器檢測到一個Servlet要被卸載,這多是由於要回收資源或者由於它正在被關閉,容器會在全部Servlet的service()線程以後,調用Servlet的destroy()方法。而後,Servlet就能夠進行無用存儲單元收集清理。這樣Servlet對象就被銷燬了。這四個階段共同決定了Servlet的生命週期。緩存
1.客戶端經過發送請求給Tomcat,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()方法)。ide
6.經過request對象取得客戶端傳過來的數據,對數據進行處理後經過response對象將處理結果寫回客戶端。
從上面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 { BigInteger i=extractFromRequest(); BigInteger[] factors=factor(i); ++count; } }
遞增操做count++並不是是原子操做,它包含了三個獨立的操做:讀取count的值,將值加1,
而後將計算結果寫入coune,這是一個「讀取-修改-寫入」的操做序列,而且其結果狀態依賴於以前的狀態。在執行時序不一樣的狀況下,可能會產生錯誤。
多線程下每一個線程對局部變量都會有本身的一份copy,這樣對局部變量的修改只會影響到本身的copy而不會對別的線程產生影響,因此這是線程安全的。
可是對於實例變量來講,因爲servlet在Tomcat中是以單例模式存在的,全部的線程共享實例變量。多個線程對共享資源的訪問就形成了線程不安全問題。
避免使用實例變量
避免使用非線程安全的集合
在多個Servlet中對某個外部對象(例如文件)的修改是務必加鎖(Synchronized,或者ReentrantLock),互斥訪問。
屬性的線程安全:ServletContext、HttpSession是線程安全的;ServletRequest是非線程安全的。
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 內存模型中,方法中的臨時變量是在棧上分配空間,並且每一個線程都有本身私有的棧空間,因此它們不會影響線程的安全。
參考: