Servlet體系結構是建立在Java多線程機制之上的它的生命周期是由Web容器負責的當客戶端第一次請求某個Servlet 時Servlet容器將會根據webxml配置文件實例化這個Servlet類當有新的客戶端請求該Servlet時一般不會再實例化該 Servlet類也就是有多個線程在使用這個實例 這樣當兩個或多個線程同時訪問同一個Servlet時可能會發生多個線程同時訪問同一資源的情況數據可能會變得不一致所以在用Servlet構建的Web應用時如果不注意線程安全的問題會使所寫的Servlet程序有難以發現的錯誤
實例變量不正確的使用是造成Servlet線程不安全的主要原因下面針對該問題給出了三種解決方案並對方案的選取給出了一些參考性的建議
實現 SingleThreadModel 接口
該接口指定了系統如何處理對同一個Servlet的調用如果一個Servlet被這個接口指定那麼在這個Servlet中的service方法將不會有兩個線程被同時執行當然也就不存在線程安全的問題這種方法只要將前面的Concurrent Test類的類頭定義更改為
Public class Concurrent Test extends HttpServlet implements SingleThreadModel {
…………
}
同步對共享數據的操作
使用synchronized 關鍵字能保證一次只有一個線程可以訪問被保護的區段在本論文中的Servlet可以通過同步塊操作來保證線程的安全同步後的代碼如下
…………
Public class Concurrent Test extends HttpServlet { …………
Username = requestgetParameter (username);
Synchronized (this){
Output = responsegetWriter ();
Try {
Thread Sleep ();
} Catch (Interrupted Exception e){}
outputprintln(用戶名:+Username+
);
}
}
}
避免使用實例變量
本實例中的線程安全問題是由實例變量造成的只要在Servlet裡面的任何方法裡面都不使用實例變量那麼該Servlet就是線程安全的
修正上面的Servlet代碼將實例變量改為局部變量實現同樣的功能代碼如下
……
Public class Concurrent Test extends HttpServlet {public void service (HttpServletRequest request HttpServletResponse
Response) throws ServletException IOException {
Print Writer output;
String username;
ResponsesetContentType (text/html; charset=gb);
……
}
}
對上面的三種方法進行測試可以表明用它們都能設計出線程安全的Servlet程序但是如果一個Servlet實現了 SingleThreadModel接口Servlet引擎將為每個新的請求創建一個單獨的Servlet實例這將引起大量的系統開銷 SingleThreadModel在Servlet中已不再提倡使用同樣如果在程序中使用同步來保護要使用的共享的數據也會使系統的性能大大下降這是因為被同步的代碼塊在同一時刻只能有一個線程執行它使得其同時處理客戶請求的吞吐量降低而且很多客戶處於阻塞狀態另外為保證主存內容和線程的工作內存中的數據的一致性要頻繁地刷新緩存這也會大大地影響系統的性能所以在實際的開發中也應避免或最小化 Servlet 中的同步代碼在Serlet中避免使用實例變量是保證Servlet線程安全的最佳選擇從Java 內存模型也可以知道方法中的臨時變量是在棧上分配空間而且每個線程都有自己私有的棧空間所以它們不會影響線程的安全
補充
servlet存在的多線程問題
實例變量: 實例變量是在堆中分配的並被屬於該實例的所有線程共享所以不是線程安全的
JSP系統提供的個類變量:
JSP中用到的OUTREQUESTRESPONSESESSIONCONFIGPAGEPAGECONXT是線程安全的APPLICATION在整個系統內被使用所以不是線程安全的
局部變量: 局部變量在堆棧中分配因為每個線程都有它自己的堆棧空間所以是線程安全的
靜態類: 靜態類不用被實例化就可直接使用也不是線程安全的
外部資源: 在程序中可能會有多個線程或進程同時操作同一個資源(如:多個線程或進程同時對一個文件進行寫操作)
此時也要注意同步問題 使它以單線程方式執行這時仍然只有一個實例所有客戶端的請求以串行方式執行這樣會降低系統的性能
對於存在線程不安全的類如何避免出現線程安全問題:
采用synchronized同步缺點就是存在堵塞問題
使用ThreadLocal(實際上就是一個HashMap)這樣不同的線程維護自己的對象線程之間相互不干擾
ThreadLocal的設計
首先看看ThreadLocal的接口
Object get() ; // 返回當前線程的線程局部變量副本 protected Object
initialValue(); // 返回該線程局部變量的當前線程的初始值
void set(Object value); // 設置當前線程的線程局部變量副本的值
ThreadLocal有個方法其中值得注意的是initialValue()該方法是一個protected
的方法顯然是為了子類重寫而特意實現的該方法返回當前線程在該線程局部變量的初始值這個方法是一個延遲調用方法在一個線程第次調用get()或者set(Object)時才執行並且僅執行次ThreadLocal中的確實實現直接返回一個null
protected Object initialValue() { return null; }
ThreadLocal是如何做到為每一個線程維護變量的副本的呢?其實實現的思路很簡單在ThreadLocal類中有一個Map用於存儲每一個線程的變量的副本比如下面的示例實現
public class ThreadLocal
{
private Map values = CollectionssynchronizedMap(new HashMap());
public Object get()
{
Thread curThread = ThreadcurrentThread();
Object o = valuesget(curThread);
if (o == null && !ntainsKey(curThread))
{
o = initialValue();
valuesput(curThread o);
}
return o;
}
public void set(Object newValue)
{
valuesput(ThreadcurrentThread() newValue);
}
public Object initialValue()
{
return null;
}
}
當然這並不是一個工業強度的實現但JDK中的ThreadLocal的實現總體思路也類似於此
ThreadLocal的使用
如果希望線程局部變量初始化其它值那麼需要自己實現ThreadLocal的子類並重寫該方法通常使用一個內部匿名類對ThreadLocal進行子類化比如下面的例子SerialNum類為每一個類分配一個序號
public class SerialNum
{
// The next serial number to be assigned
private static int nextSerialNum = ;
private static ThreadLocal serialNum = new ThreadLocal()
{
protected synchronized Object initialValue()
{
return new Integer(nextSerialNum++);
}
};
public static int get()
{
return ((Integer) (serialNumget()))intValue();
}
}
SerialNum類的使用將非常地簡單因為get()方法是static的所以在需要獲取當前線程的序號時簡單地調用
int serial = SerialNumget(); 即可
在線程是活動的並且ThreadLocal對象是可訪問的時該線程就持有一個到該線程局部變量副本的隱含引用當該線程運行結束後該線程擁有的所以線程局部變量的副本都將失效並等待垃圾收集器收集
ThreadLocal與其它同步機制的比較
ThreadLocal和其它同步機制相比有什麼優勢呢?ThreadLocal和其它所有的同步機制都是為了解決多線程中的對同一變量的訪問沖突在普通的同步機制中是通過對象加鎖來實現多個線程對同一變量的安全訪問的這時該變量是多個線程共享的使用這種同步機制需要很細致地分析在什麼時候對變量進行讀寫什麼時候需要鎖定某個對象什麼時候釋放該對象的鎖等等很多所有這些都是因為多個線程共享了資源造成的ThreadLocal就從另一個角度來解決多線程的並發訪問ThreadLocal會為每一個線程維護一個和該線程綁定的變量的副本從而隔離了多個線程的數據每一個線程都擁有自己的變量副本從而也就沒有必要對該變量進行同步了ThreadLocal提供了線程安全的共享對象在編寫多線程代碼時可以把不安全的整個變量封裝進ThreadLocal或者把該對象的特定於線程的狀態封裝進ThreadLocal
由於ThreadLocal中可以持有任何類型的對象所以使用ThreadLocal get當前線程的值是需要進行強制類型轉換但隨著新的Java版本()將模版的引入新的支持模版參數的ThreadLocal<T>類將從中受益也可以減少強制類型轉換並將一些錯誤檢查提前到了編譯期將一定程度地簡化ThreadLocal的使用
總結
當然ThreadLocal並不能替代同步機制兩者面向的問題領域不同同步機制是為了同步多個線程對相同資源的並發訪問是為了多個線程之間進行通信的有效方式而ThreadLocal是隔離多個線程的數據共享從根本上就不在多個線程之間共享資源(變量)這樣當然不需要對多個線程進行同步了所以如果你需要進行多個線程之間進行通信則使用同步機制如果需要隔離多個線程之間的共享沖突可以使用ThreadLocal這將極大地簡化你的程序使程序更加易讀簡潔
ThreadLocal常見用途
存放當前session用戶
存放一些context變量比如webwork的ActionContext
存放session比如Spring hibernate orm的session
例子用 ThreadLocal 實現每線程 Singleton
線程局部變量常被用來描繪有狀態單子(Singleton) 或線程安全的共享對象或者是通過把不安全的整個變量封裝進 ThreadLocal或者是通過把對象的特定於線程的狀態封裝進 ThreadLocal例如在與數據庫有緊密聯系的應用程序中程序的很多方法可能都需要訪問數據庫在系統的每個方法中都包含一個 Connection 作為參數是不方便的 — 用單子來訪問連接可能是一個雖然更粗糙但卻方便得多的技術然而多個線程不能安全地共享一個 JDBC Connection如清單 所示通過使用單子中的 ThreadLocal我們就能讓我們的程序中的任何類容易地獲取每線程 Connection 的一個引用這樣我們可以認為 ThreadLocal 允許我們創建每線程單子
例把一個 JDBC 連接存儲到一個每線程 Singleton 中
public class ConnectionDispenser {
private static class ThreadLocalConnection extends ThreadLocal {
public Object initialValue() {
return DriverManagergetConnection(ConfigurationSingletongetDbUrl());
}
}
private ThreadLocalConnection conn = new ThreadLocalConnection();
public static Connection getConnection() {
return (Connection) connget();
}
}
注意
理論上來說ThreadLocal是的確是相對於每個線程每個線程會有自己的ThreadLocal但是上面已經講到一般的應用服務器都會維護一套線程池因此不同用戶訪問可能會接受到同樣的線程因此在做基於TheadLocal時需要謹慎避免出現ThreadLocal變量的緩存導致其他線程訪問到本線程變量
一servlet容器如何同時處理多個請求
Servlet采用多線程來處理多個請求同時訪問Servelet容器維護了一個線程池來服務請求
線程池實際上是等待執行代碼的一組線程叫做工作者線程(Worker Thread)Servlet容器使用一個調度線程來管理工作者線程(Dispatcher Thread)
當容器收到一個訪問Servlet的請求調度者線程從線程池中選出一個工作者線程將請求傳遞給該線程然後由該線程來執行Servlet的service方法
當這個線程正在執行的時候容器收到另外一個請求調度者線程將從池中選出另外一個工作者線程來服務新的請求容器並不關系這個請求是否訪問的是同一個Servlet還是另外一個Servlet
當容器同時收到對同一Servlet的多個請求那這個Servlet的service方法將在多線程中並發的執行
二Servlet容器默認采用單實例多線程的方式來處理請求這樣減少產生Servlet實例的開銷提升了對請求的響應時間對於Tomcat可以在serverxml中通過<Connector>元素設置線程池中線程的數目
就實現來說
調度者線程類所擔負的責任如其名字該類的責任是調度線程只需要利用自己的屬性完成自己的責任所以該類是承擔了責任的並且該類的責任又集中到唯一的單體對象中
而其他對象又依賴於該特定對象所承擔的責任我們就需要得到該特定對象那該類就是一個單例模式的實現了
三如何開發線程安全的 Servlet
變量的線程安全這裡的變量指字段和共享數據(如表單參數值)
a將 參數變量 本地化多線程並不共享局部變量所以我們要盡可能的在servlet中使用局部變量
例如String user = ;
user = requestgetParameter(user);
b使用同步塊Synchronized防止可能異步調用的代碼塊這意味著線程需要排隊處理
在使用同板塊的時候要盡可能的縮小同步代碼的范圍不要直接在sevice方法和響應方法上使用同步這樣會嚴重影響性能
屬性的線程安全ServletContextHttpSessionServletRequest對象中屬性
ServletContext(線程是不安全的)
ServletContext是可以多線程同時讀/寫屬性的線程是不安全的要對屬性的讀寫進行同步處理或者進行深度Clone()
所以在Servlet上下文中盡可能少量保存會被修改(寫)的數據可以采取其他方式在多個Servlet中共享比方我們可以使用單例模式來處理共享數據
HttpSession(線程是不安全的)
HttpSession對象在用戶會話期間存在只能在處理屬於同一個Session的請求的線程中被訪問因此Session對象的屬性訪問理論上是線程安全的
當用戶打開多個同屬於一個進程的浏覽器窗口在這些窗口的訪問屬於同一個Session會出現多次請求需要多個工作線程來處理請求可能造成同時多線程讀寫屬性
這時我們需要對屬性的讀寫進行同步處理使用同步塊Synchronized和使用讀/寫器來解決
ServletRequest(線程是安全的)
對於每一個請求由一個工作線程來執行都會創建有一個新的ServletRequest對象所以ServletRequest對象只能在一個線程中被訪問ServletRequest是線程安全的
注意ServletRequest對象在service方法的范圍內是有效的不要試圖在service方法結束後仍然保存請求對象的引用
使用同步的集合類
使用Vector代替ArrayList使用Hashtable代替HashMap
不要在Servlet中創建自己的線程來完成某個功能
Servlet本身就是多線程的在Servlet中再創建線程將導致執行情況復雜化出現多線程安全問題
在多個servlet中對外部對象(比方文件)進行修改操作一定要加鎖做到互斥的訪問
四SingleThreadModel接口
javaxservletSingleThreadModel接口是一個標識接口如果一個Servlet實現了這個接口那Servlet容器將保證在一個時刻僅有一個線程可以在給定的servlet實例的service方法中執行將其他所有請求進行排隊
服務器可以使用多個實例來處理請求代替單個實例的請求排隊帶來的效益問題服務器創建一個Servlet類的多個Servlet實例組成的實例池對於每個請求分配Servlet實例進行響應處理之後放回到實例池中等待下此請求這樣就造成並發訪問的問題
此時局部變量(字段)也是安全的但對於全局變量和共享數據是不安全的需要進行同步處理而對於這樣多實例的情況SingleThreadModel接口並不能解決並發訪問問題
SingleThreadModel接口在servlet規范中已經被廢棄了
From:http://tw.wingwit.com/Article/program/Java/gj/201311/27569.html