源自 :
http://makeitjoy.javaeye.com/blog/2522981.在用Servlet時,我們都知道Servlet只會被初始化一次,只有一個實例。
2.在Struts1中,ActionServlet也僅是初始化一次,也是單實例。
為什么會這樣,無非是為了提高效率。但是線程安全不容忽視。但是在WebWork、Struts2卻做好了線程安全。
下面具體介紹:
Servlet線程安全概述
在探討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),我們必須深入它們。
什么是線程安全?
Thread-safe describes a program portion or routine that can be called from multiple programming threads without unwanted interaction between the threads。
在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:
- public?class?SimpleServlet?extends?HttpServlet ??
- { ??
- ??????
- ????private?int?counter?=?0; ??
- ??
- ????public?void?doGet(HttpServletRequest?req,?HttpServletResponse?resp) ??
- ????????????throws?ServletException,?IOException ??
- ????{ ??
- ????????doPost(req,?resp); ??
- ????} ??
- ??
- ????public?void?doPost(HttpServletRequest?req,?HttpServletResponse?resp) ??
- ????????????throws?ServletException,?IOException ??
- ????{ ??
- ????????resp.getWriter().println("<HTML><BODY>"); ??
- ????????resp.getWriter().println(this?+?"?==>?"); ??
- ????????resp.getWriter().println(Thread.currentThread()?+?":?<br>"); ??
- ???????? ??
- ????????for?(int?c?=?0;?c?<?10;?c++) ??
- ????????{ ??
- ????????????resp.getWriter().println("Counter?=?"?+?counter?+?"<BR>"); ??
- ????????????try??
- ????????????{ ??
- ????????????????Thread.sleep((long)?Math.random()?*?1000); ??
- ????????????????counter++; ??
- ????????????} ??
- ????????????catch?(InterruptedException?exc) ??
- ????????????{ ??
- ????????????} ??
- ????????} ??
- ????????resp.getWriter().println("</BODY></HTML>"); ??
- ????} ??
- }??
public class SimpleServlet extends HttpServlet
{
// A variable that is NOT thread-safe!
private int counter = 0;
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
doPost(req, resp);
}
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
resp.getWriter().println("<HTML><BODY>");
resp.getWriter().println(this + " ==> ");
resp.getWriter().println(Thread.currentThread() + ": <br>");
for (int c = 0; c < 10; c++)
{
resp.getWriter().println("Counter = " + counter + "<BR>");
try
{
Thread.sleep((long) Math.random() * 1000);
counter++;
}
catch (InterruptedException exc)
{
}
}
resp.getWriter().println("</BODY></HTML>");
}
}
然后,我們通過一個html頁面向該servlet發出三次請求:
- <HTML>??
- <BODY>??
- <TABLE>??
- <TR>??
- <TD><IFRAME?src="./SimpleServlet"?name="servlet1"?height="200%">?</IFRAME></TD>??
- </TR>??
- <TR>??
- ??
- <TD><IFRAME?src="./SimpleServlet"?name="servlet2"?height="200%">?</IFRAME></TD>??
- ??
- </TR>??
- <TR>??
- ??
- <TD><IFRAME?src="./SimpleServlet"?name="servlet3"?height="200%">?</IFRAME></TD>??
- ??
- </TR>??
- ??
- </TABLE>??
- </BODY>??
- </HTML>??
<HTML>
<BODY>
<TABLE>
<TR>
<TD><IFRAME src="./SimpleServlet" name="servlet1" height="200%"> </IFRAME></TD>
</TR>
<TR>
<TD><IFRAME src="./SimpleServlet" name="servlet2" height="200%"> </IFRAME></TD>
</TR>
<TR>
<TD><IFRAME src="./SimpleServlet" name="servlet3" height="200%"> </IFRAME></TD>
</TR>
</TABLE>
</BODY>
</HTML>
????
刷新頁面幾次后,產生的結果為:
- com.zwchen.servlet.SimpleServlet@11e1bbf?==>?Thread[http-8081-Processor23,5,main]:? ??
- Counter?=?60??
- Counter?=?61??
- Counter?=?62??
- Counter?=?65??
- Counter?=?68??
- Counter?=?71??
- Counter?=?74??
- Counter?=?77??
- Counter?=?80??
- Counter?=?83??
- ??
- ??
- ??
- com.zwchen.servlet.SimpleServlet@11e1bbf?==>?Thread[http-8081-Processor22,5,main]:? ??
- Counter?=?61??
- Counter?=?63??
- Counter?=?66??
- Counter?=?69??
- Counter?=?72??
- Counter?=?75??
- Counter?=?78??
- Counter?=?81??
- Counter?=?84??
- Counter?=?87??
- ??
- ??
- ??
- com.zwchen.servlet.SimpleServlet@11e1bbf?==>?Thread[http-8081-Processor24,5,main]:? ??
- Counter?=?61??
- Counter?=?64??
- Counter?=?67??
- Counter?=?70??
- Counter?=?73??
- Counter?=?76??
- Counter?=?79??
- Counter?=?82??
- Counter?=?85??
- Counter?=?88??
com.zwchen.servlet.SimpleServlet@11e1bbf ==> Thread[http-8081-Processor23,5,main]:
Counter = 60
Counter = 61
Counter = 62
Counter = 65
Counter = 68
Counter = 71
Counter = 74
Counter = 77
Counter = 80
Counter = 83
com.zwchen.servlet.SimpleServlet@11e1bbf ==> Thread[http-8081-Processor22,5,main]:
Counter = 61
Counter = 63
Counter = 66
Counter = 69
Counter = 72
Counter = 75
Counter = 78
Counter = 81
Counter = 84
Counter = 87
com.zwchen.servlet.SimpleServlet@11e1bbf ==> Thread[http-8081-Processor24,5,main]:
Counter = 61
Counter = 64
Counter = 67
Counter = 70
Counter = 73
Counter = 76
Counter = 79
Counter = 82
Counter = 85
Counter = 88
我們會發現三點:
servlet只產生了一個Servlet對象,因為輸出this時,其hashcode都一樣,
servlet在不同的線程(線程池)中運行,如http-8081-Processor22,http-8081-Processor23
Count被這三個doGet方法共享,并且并行修改。
上面的結果,違反了線程安全的兩個方面。
那么,我們怎樣保證按照我們期望的結果運行呢?首先,我想保證產生的count都是順序執行的。
我們將Servlet代碼重構如下:
- public?class?SimpleServlet?extends?HttpServlet ??
- ??
- { ??
- ??????
- ????private?int?counter?=?0; ??
- ????private?String?mutex?=?""; ??
- ???? ??
- ????public?void?doGet(HttpServletRequest?req,?HttpServletResponse?resp) ??
- ????throws?ServletException,?IOException ??
- ????{ ??
- ????????doPost(req,?resp); ??
- ????} ??
- ??
- ????public?void?doPost(HttpServletRequest?req,?HttpServletResponse?resp) ??
- ????throws?ServletException,?IOException ??
- ????{ ??
- ????????resp.getWriter().println("<HTML><BODY>"); ??
- ????????resp.getWriter().println(this?+?":?<br>"); ??
- ????????synchronized?(mutex) ??
- ????????{ ??
- ????????????for?(int?c?=?0;?c?<?10;?c++) ??
- ????????????{ ??
- ????????????????resp.getWriter().println("Counter?=?"?+?counter?+?"<BR>"); ??
- ????????????????try??
- ????????????????{ ??
- ????????????????????Thread.sleep((long)?Math.random()?*?1000); ??
- ????????????????????counter++; ??
- ????????????????} ??
- ????????????????catch?(InterruptedException?exc)?{ ??
- ????????????????} ??
- ????????????} ??
- ????????} ??
- ????????resp.getWriter().println("</BODY></HTML>"); ??
- ????} ??
- }??
public class SimpleServlet extends HttpServlet
{
// A variable that is NOT thread-safe!
private int counter = 0;
private String mutex = "";
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
doPost(req, resp);
}
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
resp.getWriter().println("<HTML><BODY>");
resp.getWriter().println(this + ": <br>");
synchronized (mutex)
{
for (int c = 0; c < 10; c++)
{
resp.getWriter().println("Counter = " + counter + "<BR>");
try
{
Thread.sleep((long) Math.random() * 1000);
counter++;
}
catch (InterruptedException exc) {
}
}
}
resp.getWriter().println("</BODY></HTML>");
}
}
我們的輸出結果為:
- com.zwchen.servlet.SimpleServlet@109da93:? ??
- Counter?=?0??
- Counter?=?1??
- Counter?=?2??
- Counter?=?3??
- Counter?=?4??
- Counter?=?5??
- Counter?=?6??
- Counter?=?7??
- Counter?=?8??
- Counter?=?9??
- ??
- ??
- ??
- com.zwchen.servlet.SimpleServlet@109da93:? ??
- Counter?=?10??
- Counter?=?11??
- Counter?=?12??
- Counter?=?13??
- Counter?=?14??
- Counter?=?15??
- Counter?=?16??
- Counter?=?17??
- Counter?=?18??
- Counter?=?19??
- ??
- ??
- ??
- com.zwchen.servlet.SimpleServlet@109da93:? ??
- Counter?=?20??
- Counter?=?21??
- Counter?=?22??
- Counter?=?23??
- Counter?=?24??
- Counter?=?25??
- Counter?=?26??
- Counter?=?27??
- Counter?=?28??
- Counter?=?29??
com.zwchen.servlet.SimpleServlet@109da93:
Counter = 0
Counter = 1
Counter = 2
Counter = 3
Counter = 4
Counter = 5
Counter = 6
Counter = 7
Counter = 8
Counter = 9
com.zwchen.servlet.SimpleServlet@109da93:
Counter = 10
Counter = 11
Counter = 12
Counter = 13
Counter = 14
Counter = 15
Counter = 16
Counter = 17
Counter = 18
Counter = 19
com.zwchen.servlet.SimpleServlet@109da93:
Counter = 20
Counter = 21
Counter = 22
Counter = 23
Counter = 24
Counter = 25
Counter = 26
Counter = 27
Counter = 28
Counter = 29
這符合了我們的要求,輸出都是按順序的,這正式synchronized的含義。
附帶說一下,我現在synchronized的是一個字符串變量mutex,不是this對象,這主要是從performance和Scalability考慮。Synchronized用在this對象上,會帶來嚴重的可伸縮性的問題(Scalability),所有的并發請求都要排隊!
現在,我們保證了順序,但是我們怎么保證Counter字段(不是局部變量!)在每個Servlet的線程下都是獨立的呢?也就是說,并發請求時,它們都不相互干擾。
我現在將Servlet代碼重構如下:
- public?class?SimpleServlet?extends?HttpServlet ??
- { ??
- ????private?ThreadLocal?counter?=?new?ThreadLocal()?{ ??
- ????????protected?synchronized?Object?initialValue() ??
- ????????{ ??
- ????????????return?new?Integer(0); ??
- ????????} ??
- ????}; ??
- ??
- ????public?void?doGet(HttpServletRequest?req,?HttpServletResponse?resp) ??
- ????????????throws?ServletException,?IOException ??
- ????{ ??
- ????????doPost(req,?resp); ??
- ????} ??
- ??
- ????public?void?doPost(HttpServletRequest?req,?HttpServletResponse?resp) ??
- ????????????throws?ServletException,?IOException ??
- ????{ ??
- ????????resp.getWriter().println("<HTML><BODY>"); ??
- ????????resp.getWriter().println( ??
- ????????????????this?+?"["?+?Thread.currentThread()?+?"]:?<br>"); ??
- ????????for?(int?c?=?0;?c?<?10;?c++) ??
- ????????{ ??
- ????????????resp.getWriter().println(counter.get()?+?"<br>"); ??
- ????????????try??
- ????????????{ ??
- ????????????????Thread.sleep((long)?Math.random()?*?1000); ??
- ????????????????int?c1?=?((Integer)?counter.get()).intValue(); ??
- ????????????????c1++; ??
- ????????????????counter.set(new?Integer(c1)); ??
- ????????????} ??
- ????????????catch?(InterruptedException?exc) ??
- ????????????{ ??
- ????????????} ??
- ????????} ??
- ??
- ????????resp.getWriter().println("</BODY></HTML>"); ??
- ????} ??
- }??
public class SimpleServlet extends HttpServlet
{
private ThreadLocal counter = new ThreadLocal() {
protected synchronized Object initialValue()
{
return new Integer(0);
}
};
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
doPost(req, resp);
}
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
resp.getWriter().println("<HTML><BODY>");
resp.getWriter().println(
this + "[" + Thread.currentThread() + "]: <br>");
for (int c = 0; c < 10; c++)
{
resp.getWriter().println(counter.get() + "<br>");
try
{
Thread.sleep((long) Math.random() * 1000);
int c1 = ((Integer) counter.get()).intValue();
c1++;
counter.set(new Integer(c1));
}
catch (InterruptedException exc)
{
}
}
resp.getWriter().println("</BODY></HTML>");
}
}
現在,我刷新html頁面三次,第三次結果如下:
- com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor22,5,main]]:? ??
- 20??
- 21??
- 22??
- 23??
- 24??
- 25??
- 26??
- 27??
- 28??
- 29??
- ??
- com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor25,5,main]]:? ??
- 20??
- 21??
- 22??
- 23??
- 24??
- 25??
- 26??
- 27??
- 28??
- 29??
- ??
- com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor23,5,main]]:? ??
- 20??
- 21??
- 22??
- 23??
- 24??
- 25??
- 26??
- 27??
- 28??
- 29??
com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor22,5,main]]:
20
21
22
23
24
25
26
27
28
29
com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor25,5,main]]:
20
21
22
23
24
25
26
27
28
29
com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor23,5,main]]:
20
21
22
23
24
25
26
27
28
29
從以上結果,我們可以發現:
1、在該html頁面內的并發三次請求中,該Servlet里面的counter字段都不相互干擾
2、counter字段還是實例字段,并且都會保留狀態,不是每次都用0開始
3、html頁面內的三次請求都在不同的線程,但在同一個實例中。
總之,在Java里面,字段(不是局部變量)有三個共享范圍:instance field,static field,local thread field,而后者往往在服務器端這種多線程環境必須考慮到的。
??
在J2EE項目開發過程中,ThreadLocal類有時有非常重要的作用,下面是我碰到的,但可以延伸:
1、在用Hibernate做web開發的持久化時,有個模式叫做Open Session In View,也就是將session保留到頁面中,在response結束后,在OpenSessionInViewFilter中關閉session,這對于延遲加載非常有效,例如,我們在頁面上顯示User的詳細信息,需要顯示該user的所屬Department的信息; 但是,在list users這種不需要顯示department信息的地方,那個user的department信息就不會加載,也就是說加載相關信息是動態的,但不會出現LazyInitializationException,也就是Load on demand。不過,注意慎用該模式。
2、在工作流開發,例如OSWorflow,每次調用其服務前,都需要將caller對象傳入,這樣會導致我們的方法非常臃腫,如果我們在調用該方法的上層,如在Servlet里調用它之前,將User對象置于ThreadLocal中,那么可以在工作流方法內通過get()方法獲取,而不用傳入參數。
3、為什么Web框架中,Webwork的action中可以有field,但Struts卻不能?其實,也就是說,Struts不是線程安全的,而Webwork是線程安全的。大家可以參考Webwork的ActionContext類:
- public?class?ActionContext?implements?Serializable?{ ??
- ??
- static?ThreadLocal?actionContext?=?new?ActionContextThreadLocal(); ??
- ??
- ……………??
public class ActionContext implements Serializable {
static ThreadLocal actionContext = new ActionContextThreadLocal();
……………
而對于Struts,我們可以從ActionServlet.process() => RequestProcessor. processActionPerform,在RequestProcessor中有字段?? protected HashMap actions = new HashMap();我們不難發現,我們所寫的action是共享的,那么內部字段必然也是共享。注意,這種共享類似于Servlet里面的字段。