本文主要來源 zwchen的博客:http://zwchen.iteye.com/blog/91088
概述
在探討java線程安全前,讓我們先簡要介紹一下Java語言。
任何語言,如C++,C#,Java,它們都有相通之處,特別是語法,但如果有人問你,Java語言的核心是什么?類庫?關鍵字?語法?似乎都不 是。Java語言的核心,也就是Sun始終不願意開源的東西:Java虛擬機的實現(不過sun公開了其Java虛擬機規范),也就有了BEA的 JRockit,IBM的Jikes,Sun的Hotspot。
Java的核心有兩點,Java類加載(Java Class Loader)和Java內存管理,它們具體體現在Java類庫的以下幾個類:
java.lang.ClassLoader(java.lang.Class):我們調用的類,包括其接口和超類,import的類是怎么被Java虛擬機載入的?為什么static的字段在servlet容器里面可以一直生存下去(Spring容器中)?
java.lang.Thread(java.lang.ThreadLocal):垃圾回收是怎么進行的(垃圾回收線程)?我們的程序是怎么退出的?
java.lang.refelect.Proxy(java.lang.refelect.Method):為什么Tomcat、 Tapestry、Webwork、Spring等容器和框架可以通過配置文件來調用我們寫的類?Servlet規范、JSF規范、EJB規范、JDBC 規范究竟是怎么回事?為什么它們幾乎都是一些接口,而不是具體類?
Servlet線程安全
在Java的server side開發過程中,線程安全(Thread Safe)是一個尤為突出的問題。因為容器,如Servlet、EJB等一般都是多線程運行的。雖然在開發過程中,我們一般不考慮這些問題,但診斷問題 (Robust),程序優化(Performance),我們必須深入它們。
什么是線程安全?
在Java里,線程安全一般體現在兩個方面:
1、多個thread對同一個java實例的訪問(read和modify)不會相互干擾,它主要體現在關鍵字synchronized。如 ArrayList和Vector,HashMap和Hashtable(后者每個方法前都有synchronized關鍵字)。如果你在 interator一個List對象時,其它線程remove一個element,問題就出現了。
2、每個線程都有自己的字段,而不會在多個線程之間共享。它主要體現在java.lang.ThreadLocal類,而沒有Java關鍵字支持,如像static、transient那樣。
一個普遍的疑問,我們的Servlet中能夠像JavaBean那樣declare instance或static字段嗎?如果不可以?會引發什么問題?
答案是:不可以。我們下面以實例講解:
首先,我們寫一個普通的Servlet,里面有instance字段count:
web.xml >>
1 <servlet> 2 <servlet-name>SimpleServlet</servlet-name> 3 <servlet-class>servlet.SimpleServlet</servlet-class> 4 </servlet> 5 <servlet-mapping> 6 <servlet-name>SimpleServlet</servlet-name> 7 <url-pattern>/SimpleServlet</url-pattern> 8 </servlet-mapping>
SimpleServlet >>
1 public class SimpleServlet extends HttpServlet { 2 private int counter = 0; 3 @Override 4 protected void service(HttpServletRequest request, HttpServletResponse response) 5 throws ServletException, IOException { 6 response.getWriter().println("<HTML><BODY>"); 7 response.getWriter().println(this + " ==> "); 8 response.getWriter().println(Thread.currentThread() + ": <br>"); 9 for(int c=0;c<10;c++){ 10 response.getWriter().println("Counter = " + counter + "<BR>"); 11 try { 12 Thread.sleep(1000); 13 counter++; 14 } catch (Exception e) { 15 e.printStackTrace(); 16 } 17 } 18 response.getWriter().println("</BODY></HTML>"); 19 } 20 }
test.html >>
1 <HTML> 2 <BODY> 3 <TABLE> 4 <TR> 5 <TD><IFRAME src="SimpleServlet" name="servlet1" height="200%"> </IFRAME></TD> 6 </TR> 7 </TABLE> 8 </BODY> 9 </HTML>
大家應該發現,test.html寫的和zwchen的博客原文中的寫的有點區別,本來也是按照zwchen的博客原文中的去測試的,但是相信很多人並沒有得出理想的結果,正如博客下面評論上5樓所說的:“沒有出現線程安全問題,數字的順序都是正確的”,我也是如此(我用的是Firefox瀏覽器)。后來換了IE瀏覽器進行測試出現下面的問題,在頁面上只顯示出了第一個<tr></tr>里面的內容,於是我的處理方法就是:test.html的內容如上所示,打開3個IE瀏覽器,同時在瀏覽器中輸入:
a: http://localhost:8080/ServletTest/SimpleServlet
b: http://localhost:8080/ServletTest/SimpleServlet
c: http://localhost:8080/ServletTest/SimpleServlet
測試結果如下:
我們會發現三點:
1、Servlet是一個單例對象(Singleton),因為我們看到多次請求的this指針所有打印出來的hashCode值都相同。
2、servlet在不同的線程(線程池)中運行,如http-8080-1,http-8080-2,http-8080-3 等輸出值可以明顯區分出不同的線程執行了不同一段Servlet邏輯代碼。
3、count變量在不同的線程中共享,而且它的值被不同的線程修改,輸出時已經不是順序輸出。也就是說,其他的線程會篡改當前線程中實例變量的值,針對這些對象的訪問不是線程安全的。
上面的結果,違反了線程安全的兩個方面。
那么,我們怎樣保證按照我們期望的結果運行呢?首先,我想保證產生的count都是順序執行的。
我們將Servlet代碼重構如下:
1 public class SimpleServlet extends HttpServlet { 2 private int counter = 0; 3 private String mutex = ""; 4 @Override 5 protected void service(HttpServletRequest request, HttpServletResponse response) 6 throws ServletException, IOException { 7 response.getWriter().println("<HTML><BODY>"); 8 response.getWriter().println(this + " ==> "); 9 response.getWriter().println(Thread.currentThread() + ": <br>"); 10 synchronized (mutex){ 11 for(int c=0;c<10;c++){ 12 response.getWriter().println("Counter = " + counter + "<BR>"); 13 try { 14 Thread.sleep(1000); 15 counter++; 16 } catch (Exception e) { 17 e.printStackTrace(); 18 } 19 } 20 } 21 response.getWriter().println("</BODY></HTML>"); 22 } 23 }
這符合了我們的要求,輸出都是按順序的,這正式synchronized的含義。
附帶說一下,我現在synchronized的是一個字符串變量mutex,不是this對象,這主要是從performance和 Scalability考慮。Synchronized用在this對象上,會帶來嚴重的可伸縮性的問題(Scalability),所有的並發請求都要排隊!