前言:前面說了很多關於Servlet的一些基礎知識,這一篇主要說一下關於Servlet的線程安全問題。
要想弄清Servlet線程安全我們必須先要明白Servlet實例是如何創建,它的模式是什麼樣的。
在默認的情況下Servlet容器對聲明的Servlet,只創建一個Servlet實例,那麼如果要是多個客戶同時請求訪問這個Servlet,Servlet容器就采取多線程。下面我們來看一幅圖
從圖中可以看出當客戶發送請求的時候,Servlet容器通過調度者線程從線程池中選擇一個線程,然後將請求傳遞給這個線程,然後在由這個線程去執行Servlet的Service方法。
如果多個客戶端同時請求執行一個Servlet實例,那麼這個Servlet容器的Service方法將在多個線程中並發執行(比喻圖中客戶1,客戶2,客戶3同時調用Servlet1實例,那麼調度者線程就會在線程池中調用3個線程分別用於客戶1,2,3的請求,然後3個線程同時並發執行Servlet1實例的Service方法)因為Servlet容器采取的單實例多線程的方法,那麼就大大的減小了Servlet實例創建的開銷,提升了對請求的響應時間,也是這樣引起了Servlet線程安全問題。所以我們下面說線程安全問題。
我們先看一段代碼
1 public class HelloWorldServlet extends HttpServlet{ 2 private String userName; 3 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException 4 { 5 userName=request.getParameter("userName"); 6 PrintWriter out=response.getWriter(); 7 if(userName!=null&&userName!="") 8 { 9 out.print(userName); 10 } 11 else { 12 out.println("用戶名不存在"); 13 } 14 } 15 }
我們來分析這段代碼,現在有A,B2個客戶端同時請求HelloWorldServlet 這個實例,Servlet容器分配線程T1來服務A客戶端的請求,T2來服務B客戶端的請求,操作系統首先調用T1來運行,T1運行到第6行的時候得到了用戶名為張三並保存,此時時間片段到了,操作系統開始調用T2運行也運行到第6行但是這個用戶名是李四,此時時間片段又到了,操作系統又開始運行T1,從第7行開始運行,但是此時的用戶名卻成了李四,輸出的時候確實李四(很明顯是錯誤的),那麼這個時候就出現了線程安全問題。
protected synchronized void doGet(HttpServletRequest request, HttpServletResponse response)
采用這種方式明顯不合適,因為這樣T2必須要等T1執行完畢以後才可以執行,大大的影響了效率。
3.如果是靜態資源則加上final表示這個資源不可以改變
比喻 final static String url="jdbc:mysql://localhost:3306/blog";
在Servlet中可以訪問保存在ServletContext,HttpSession,ServletRequest對象中的屬性,這三種對象都提供了getAttribute(),setAttribute() 方法用來對取和設置屬性,那麼這三個不同范圍對象的屬性訪問是否線程安全呢,下面我們來一起看一下
首先明確一點是ServletContext是被應用程序下所有的Servlet所共享的,那麼ServletContext對象就可以被web應用程序所有的Servlet訪問,那麼這樣一來多個Servlet就可以同時對ServletContext的屬性進行設置和訪問,所以這個時候就會出現線程安全問題。我們來看一段代碼
1 protected void service(HttpServletRequest request, HttpServletResponse response) 2 { 3 String userName=request.getParameter("userName"); 4 if ("login") { 5 List list=(List)getServletContext().getAttribute("userList"); 6 list.add(userName); 7 } 8 else { 9 List list=(List)getServletContext().getAttribute("userList"); 10 list.remove(userName); 11 } 12 }
1 protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException 2 { 3 List list=(List)getServletContext().getAttribute("userList"); 4 int count=list.size(); 5 for(int i=0;i<count;i++) 6 { 7 PrintWriter out=response.getWriter(); 8 out.println(list.get(i)); 9 } 10 }
第一段代碼是當用戶登錄以後把用戶名保存在ServletContext屬性中,如果不是登錄就刪除這個用戶
第二段代碼就是查看應用程序所有的用戶登錄情況,那麼我們看如何出現線程安全問題的
當2個請求並發執行的時候,可能第二段代碼剛剛執行第五行的時候獲取的count=5;但是呢另一個請求恰好執行第一段代碼第十行,把其中的某個用戶刪除了,當第二段代碼在循環遍歷的時候運行到count=5的時候就會數組超過索性界限異常。那麼此時就出現了線程安全問題。那麼遇到這樣的問題怎麼解決呢,第一就是把ServletContext屬性值進行拷貝保存起來,第二就是采用synchronized 進行同步(這個效率低)
httpSession對象在用戶會話期間存活的,不像ServletContext一樣被所有的用戶共享,所以說一個HttpSession在同一個時刻只用一個用戶進行請求的,因此理論看來Session是線程安全的,其實並不是如此,這個和浏覽器有關,在上一篇Session我們說過,同一個浏覽器只能具有一個Session,那麼這樣一來就會出現Session線程安全問題,看如下代碼
1 protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException 2 { 3 String commandType=request.getParameter("commandType"); 4 HttpSession session=request.getSession(); 5 List list=(List)session.getAttribute("items"); 6 if ("add".equals(commandType)) { 7 //添加 8 } 9 else if("delete".equals(commandType)){ 10 //刪除 11 } 12 else { 13 int count=list.size(); 14 for (int i = 0; i < count; i++) { 15 //遍歷 16 } 17 } 18 }
上面是一個添加物品信息的一個簡單偽代碼,如果用戶現在在一個浏覽器窗口刪除一件物品的同時又在另一個窗口去獲取所有的物品這個時候就會出現線程安全,從上面的介紹得知Servlet容器是多線程單實例的,這個時候Servlet容器就會分配2個線程來分別為刪除物品和獲取所有物品進行服務,如果其中一個線程剛好運行到14行時間片段結束,另一個線程這個時候又運行第10行刪除一條物品信息,然後第一個線程又開始運行第15開始遍歷,此時同樣出現了上面數組索性超出范圍的錯誤。
httprequest是線程安全的,因為每個請求都會調用Service,都會創建一個新的HttpRequest和局部變量一樣。
從名字很好理解,就是單線程模式,也就是說如果Servlet實現了SingleThreadModel接口,Servlet容器就保證一個時刻只有一個線程在Servlet實例的Service方法運行(其實和同步差不多)這樣一來就很影響效率了,現在SingleThreadModel已經被廢棄了,值得注意的是就算Servlet實現了SingleThreadModel接口並不一定保證線程安全,比喻上面說的ServletContext,HttpSession,因為ServletContext是應用程序共享的,可能2個Servlet實例同時運行造成線程安全,HttpSession因為是在同一浏覽器共享的所以也會出現(雖然可能性很小)
1:只要我們了解Servlet容器工作的模式,可能就能夠理解為什麼Servlet會出現線程安全問題,所以一定牢記Servlet容器是多線程單實例的模型
2:避免使用全局變量,最好是使用局部變量,其實這本身也是一個好的編程習慣
3:應該使用只讀的實例變量和靜態變量(就是前面加上final意為不可改變)
4:不要在Servlet上自己創建線程,因為Servlet容器已經幫我們做好了。
5:如果要修改共享對象的時候記得要同步,盡量縮小同步的范圍(比喻修改Session時候直接使用synchronized(Session)即可),避免影響性能