前言
在上一篇關於Serlvet框架和Servlet生命周期的學習中,我們已經知道了在多線程的情況下
Servlet是線程不安全的。Servlet體系是建立在java多線程的基礎之上的,它的生命周期是由Tomcat
來維護的。當客戶端第一次請求Servlet的時候,tomcat會根據web.xml配置文件實例化servlet,
當又有一個客戶端訪問該servlet的時候,不會再實例化該servlet,也就是多個線程在使用這個實例。
Servlet線程池
serlvet采用多線程來處理多個請求同時訪問,Tomcat容器維護了一個線程池來服務請求。
線程池實際上是等待執行代碼的一組線程叫做工作組線程(Worker Thread),Tomcat容器使用一個
調度線程來管理工作組線程(Dispatcher Thead)。
當容器收到一個Servlet請求,Dispatcher線程從線程池中選出一個工作組線程,將請求傳遞
給該線程,然后由該線程來執行Servlet的service方法。
當這個線程正在執行的時候,容器收到另一個請求,調度者線程將從線程池中選出另外一個
工作組線程來服務則個新的請求,容器並不關心這個請求是否訪問的是同一個Servlet還是另一個
Servlet。當容器收到對同一個Servlet的多個請求的時候,那這個servlet的service方法將在多線程
中並發的執行。
Servlet線程安全問題
多線程和單線程Servlet具體區別:多線程下每個線程對局部變量都會有自己的一份copy,這
樣對局部變量的修改只會影響到自己的copy而不會對別的線程產生影響,線程安全的。但是對於
實例變量來說,由於servlet在Tomcat中是以單例模式存在的,所有的線程共享實例變量。多個線程
對共享資源的訪問就造成了線程不安全問題。
對於單線程而言就不存在這方面的問題(static變量除外)
這里我們寫一個實例來模擬一下:
package com.kiritor; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class ThreadServlet */ public class ThreadServlet extends HttpServlet { private static final long serialVersionUID = 1L; private String message; /** * @see HttpServlet#HttpServlet() */ public ThreadServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse * response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request, response); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse * response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { message = request.getParameter("message"); PrintWriter printWriter = response.getWriter(); try { Thread.sleep(5000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } printWriter.write(message); } }
之后我們在打開兩個瀏覽器:
http://localhost:8080/Servlet03/ThreadServlet?message=helloA
http://localhost:8080/Servlet03/ThreadServlet?message=helloB
我們不斷的嘗試刷新瀏覽器,可以發現的是輸出結果並不是我們想象的那么簡單,而且錯誤的輸出
是具有偶然性的,這更增加了程序潛在的危險性。
至於其實際的輸出效果筆者就貼圖了,讀者可自行進行演示。
針對上述的情況如何設計線程安全的Servlet呢?我們知道的是多線程是不共享局部變量的
servlet線程不安全也是針對於共享資源的訪問才產生的。 因此這里就有一種方式了。
變量的線程安全
這里的變量變量指的是字段和共享數據,主要是表單的參數值。基於多線程不共享局部變量的
特點我們可以將這類變量參數本地化。例如對於上面的一個實例我們可以這樣設計。
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String message; message = request.getParameter("message"); PrintWriter printWriter = response.getWriter(); try { Thread.sleep(5000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } printWriter.write(message); }
屬性的線程安全
ServletContext:它是線程不安全的,多線程下可以同時進行讀寫,因此我們要對其讀寫操作進行
同步或者深度的clone。
HttpSession:同樣是線程不安全的,和ServletContext的操作一樣。
ServletRequest:它是線程安全的,對於每一個請求由一個工作線程來執行,都會創建一個
ServletRequest對象,所以ServletResquest只能在一個線程中被訪問,而且他只在service()方法內是
有效的。
同步的集合類
在使用java中的集合API進行處理的時候,選擇同步的集合。
外部對象互斥
在多個Servlet中對某個外部對象(例如文件)的修改是務必加鎖,互斥訪問。不過這里需要注意的是
使用Synchronized的時候這意味着線程需要排隊等待處理,因此在使用同步塊的時候要盡量的縮小同
步塊的代碼范圍。不要直接在方法上用同步,這樣會嚴重影響性能。
值得一提的是最好別再serlvet中創建自己的線程來完成某個功能,這會是情況更加復雜。
Single ThreadMode接口
這也是解決servlet線程安全問題的一個方法,Single ThreadMode是一個標識接口,如果一個Servlet
實現了該接口,那么Tomcat將保證在一個時刻僅有一個線程可以在給定的Serlvet實例的service方法中
執行。其他所有請求進行排隊。(針對單個實例)
可以看出的是這種方式雖然可以解決線程安全問題,可以效率太過低下。
其再Servlet的規范中已經被廢棄了。
總結
Servlet的線程安全問題只有在大量的並發訪問時才會顯現出來,並且很難發現,因此在編寫Servlet程序
時要特別注意。線程安全問題主要是由實例變量造成的,因此在Servlet中應避免使用實例變量。如果應用程
序設計無法避免使用實例變量,那么使用同步來保護要使用的實例變量,但為保證系統的最佳性能,應該
同步可用性最小的代碼路徑。