Servlet執行緒安全問題(轉載)
轉載地址:https://www.cnblogs.com/LipeiNet/p/5699944.html
前言:前面說了很多關於Servlet的一些基礎知識,這一篇主要說一下關於Servlet的執行緒安全問題。
1:多執行緒的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執行緒安全問題。所以我們下面說執行緒安全問題。
2:Servlet的執行緒安全
2.1:變數的執行緒安全
2.1.1:變數為啥會存線上程安全
我們先看一段程式碼
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行開始執行,但是此時的使用者名稱卻成了李四,輸出的時候確實李四(很明顯是錯誤的),那麼這個時候就出現了執行緒安全問題。
2.1.2:如何防止變數的執行緒安全
- 把全域性變數改為區域性變數
-
public class myservlet2 extends HttpServlet{
int num=1;
//servlet->GenericServlet->HttpServlet
@Override
public void init() throws ServletException {
// TODO Auto-generated method stub
super.init();
System.out.println("HttpServlet...init...初始化");
}
@Override
public void service(ServletRequest arg0, ServletResponse arg1)
throws ServletException, IOException {
// TODO Auto-generated method stub
num++;
System.out.println(num);
System.out.println(arg0.getRemoteAddr());
System.out.println("HttpServlet...servlet demo2");
}@Override
public void destroy() {
// TODO Auto-generated method stub
super.destroy();
System.out.println("HttpServlet...destory...結束");
}}
因為每次呼叫這個方法的時候就會重寫對userName例項化這樣一來就不會存線上程安全問題了。
-
- 使用synchronized對doGet進行同步
protected synchronized void doGet(HttpServletRequest request, HttpServletResponse response)
採用這種方式明顯不合適,因為這樣T2必須要等T1執行完畢以後才可以執行,大大的影響了效率。
3.如果是靜態資源則加上final表示這個資源不可以改變
比喻 final static String url="jdbc:mysql://localhost:3306/blog";
2.2:屬性的執行緒安全
在Servlet中可以訪問儲存在ServletContext,HttpSession,ServletRequest物件中的屬性,這三種物件都提供了getAttribute(),setAttribute() 方法用來對取和設定屬性,那麼這三個不同範圍物件的屬性訪問是否執行緒安全呢,下面我們來一起看一下
2.2.1:ServletContext
首先明確一點是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 進行同步(這個效率低)
2.2.2:HttpSession
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開始遍歷,此時同樣出現了上面陣列索性超出範圍的錯誤。
2.2.3:HttpRequest
httprequest是執行緒安全的,因為每個請求都會呼叫Service,都會建立一個新的HttpRequest和區域性變數一樣。
3:SingleThreadModel
從名字很好理解,就是單執行緒模式,也就是說如果Servlet實現了SingleThreadModel介面,Servlet容器就保證一個時刻只有一個執行緒在Servlet例項的Service方法執行(其實和同步差不多)這樣一來就很影響效率了,現在SingleThreadModel已經被廢棄了,值得注意的是就算Servlet實現了SingleThreadModel介面並不一定保證執行緒安全,比喻上面說的ServletContext,HttpSession,因為ServletContext是應用程式共享的,可能2個Servlet例項同時執行造成執行緒安全,HttpSession因為是在同一瀏覽器共享的所以也會出現(雖然可能性很小)
4:總結
1:只要我們瞭解Servlet容器工作的模式,可能就能夠理解為什麼Servlet會出現執行緒安全問題,所以一定牢記Servlet容器是多執行緒單例項的模型
2:避免使用全域性變數,最好是使用區域性變數,其實這本身也是一個好的程式設計習慣
3:應該使用只讀的例項變數和靜態變數(就是前面加上final意為不可改變)
4:不要在Servlet上自己建立執行緒,因為Servlet容器已經幫我們做好了。
5:如果要修改共享物件的時候記得要同步,儘量縮小同步的範圍(比喻修改Session時候直接使用synchronized(Session)即可),避免影響效能