標籤:threadlocal threadlocalmap hibernate session thread
最近在開發過程中,在做一個字典項服務的時候,最開始採用了ThreadLocal對象來快取資料。在使用ThreadLocal過程中遇到一些問題,這裡和大家分享一下。
一、
什麼是ThreadLocal?
顧名思義它是local variable(線程局部變數)。它的功用非常簡單,就是為每一個使用該變數的線程都提供一個變數值的副本。從線程的角度看,就好像每一個線程都完全擁有該變數。
它主要由四個方法組成initialValue(),get(),set(T),remove(),其中initialValue()方法是一個protected的方法,只有在重寫ThreadLocal的時候有用。
void set(T t):為調用該方法的線程存入一個本線程變數。
T get(): 返回本線程存入ThreadLocal中的值,沒有返回空。
void remove(): 移除本線程存入ThreadLocal中的值。
T initialValue():用於在為null時,產生一個初始值,ThreadLocal直接返回一個null值。
二、 ThreadLocal的原理
在查看了java源碼後發現,ThreadLocal通過使用ThreadLocalMap(註:這裡的Map非java.util.Map子類)執行個體來儲存”線程局部變數”,當第一次設值的時候,如果map為空白,則建立一個map並set入值,但是這個儲值的Map並非ThreadLocal的成員變數,而是java.lang.Thread 類的成員變數。ThreadLocal的set,get方法源碼如下:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value);} public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue();}
程式碼片段1
三、
ThreadLocal的對象釋放問題
3.1 在我們使用ThreadLocal過程中,線程結束後,它的”線程局部變數”是如何回收的呢?
首先,儲存”線程局部變數”的map並非是ThreadLocal的成員變數, 而是java.lang.Thread的成員變數。也就是說,線程結束的時候,該map的資源也同時被回收。
解析:
ThreadLocal的set,get方法中均通過如下方式擷取Map:
ThreadLocalMap map = getMap(t);
而getMap方法的代碼如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
程式碼片段2
可見:ThreadLocalMap執行個體是作為java.lang.Thread的成員變數儲存的,每個線程有唯一的一個threadLocalMap。這個map以ThreadLocal對象為key,”線程局部變數”為值,所以一個線程下可以儲存多個”線程局部變數”。對ThreadLocal的操作,實際委託給當前Thread,每個Thread都會有自己獨立的ThreadLocalMap執行個體,儲存的倉庫是Entry[] table;Entry的key為ThreadLocal,value為儲存內容;因此在並發環境下,對ThreadLocal的set或get,不會有任何問題。以下為”線程局部變數”的儲存圖:
“線程局部變數”的儲存圖
由於treadLocalMap是java.util.Thread的成員變數,threadLocal作為threadLocalMap中的key值,在一個線程中只能儲存一個”線程局部變數”。將ThreadLocalMap作為Thread類的成員變數的好處是:
a. 當線程死亡時,threadLocalMap被回收的同時,儲存的”線程局部變數”如果不存在其它引用也可以同時被回收。
b. 同一個線程下,可以有多個treadLocal執行個體,儲存多個”線程局部變數”。
3.2 如果線程線上程池中,一直存在,而threadLocal在多個地方被迴圈放入,會不會造成threadLocal對象無法回收?
如下所示:
public class TestMain {public static void main(String[] args) {while (true) {for (int j = 0; j < 10; j++) {new ThreadLocalDomail(new byte[1024*1024]).getAndPrint();}}}}class ThreadLocalDomail{private ThreadLocal<byte[]> threadLocal=new ThreadLocal< byte[]>();publicThreadLocalDomail(byte[] b){ threadLocal.set(b);}public byte[] getAndPrint(){ byte[] b=threadLocal.get(); System.out.println(b.length); return b;}}
程式碼片段3
因為ThreadLocalMap的Entry是(weakReference)弱引用,在外部不再引用threadLocal對象時,線程map中threadLocal對應的key及其value均會被釋放,不會造成記憶體溢出。以上TestMain代碼中的new ThreadLocalDomail在每次迴圈後即被丟棄,可被記憶體回收行程回收,代碼可持續運行,不會記憶體溢出。
四、
ThreadLocal的應用
在比較熟悉的兩個架構中,Struts2和Hibernate均有採用ThreadLocal變數,而且對整個架構來說是非常核心的一部分。
Struts2和Struts1的一個重要升級就是對request,response兩個對象的解耦,Struts2的Action方法中不再需要傳遞request,response參數。但是Struts2不通過方法直接傳入request,response對象,那麼這兩個值是如何傳遞的呢?
Struts2採用的正是ThreadLocal變數。在每次接收到請求時,Struts2在調用攔截器和action前,通過將request,response對象放入ActionContext執行個體中,而ActionContext執行個體是作為”線程局部變數”存入ThreadLocal actionContext中。
public class ActionContext implements Serializable { static ThreadLocal actionContext = new ThreadLocal();. . .
程式碼片段4
由於actionContext是”線程局部變數”,這樣我們通過ServletActionContext.getRequest()即可獲得本線程的request對象,而且在本地線程的任意類中,均可通過該方法擷取”線程局部變數”,而無需值傳遞,這樣Action類既可以成為一個simple類,無需繼承struts2的任意父類。
在利用Hibernate開發DAO模組時,我們和Session打的交道最多,所以如何合理的管理Session,避免Session的頻繁建立和銷毀,對於提高系統的效能來說是非常重要的。一般常用的Hibernate工廠類,都會通過ThreadLocal來儲存線程的session,這樣我們在同一個線程中的處理,工廠類的getSession()方法,即可以多次擷取同一個Session進行操作,closeSession方法可在不傳入參數的情況下,正確關閉session。
五、 使用
ThreadLocal發生的問題
在WEB伺服器環境下,由於Tomcat,weblogic等伺服器有一個線程池的概念,即接收到一個請求後,直接從線程池中取得線程處理請求;請求響應完成後,這個線程本身是不會結束,而是進入線程池,這樣可以減少建立線程、啟動線程的系統開銷。
由於Tomcat線程池的原因,我最初使用的”線程局部變數”儲存的值,在下一次請求依然存在(同一個線程處理),這樣每次請求都是在本線程中取值而不是去memCache中取值,如果memCache中的資料發生變化,也無法及時更新。
解決方案: 處理完成後主動調用該業務treadLocal的remove()方法,將”線程局部變數”清空,避免本線程下次處理的時候依然存在舊資料。由於主動清理需要使用struts2攔截器,為了簡單的解決問題,最後通過ServletActionContext.getRequest()擷取request後,將資料setAttribute進request對象中,美中不足的是和request對象有一定的耦合。
Sturts2是如何解決線程池的問題呢?
由於web伺服器的線程是多次使用的,很顯然Struts2在響應完成後,會主動的清除“線程局部變數”中的ActionContext值,在struts2的org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter類中,有這樣的程式碼片段:
finally {
prepare.cleanupRequest(request);
}
而cleanupRequest方法中有如下代碼
public void cleanupRequest(HttpServletRequest request) {……//省略部分代碼 ActionContext.setContext(null); Dispatcher.setInstance(null); }
程式碼片段6
由此可見,Sturts2在處理完成後,會主動清空”線程局部變數”ActionContext,來達到釋放系統資源的目的。
六、 總結
使用ThreadLocal的幾點建議:
1. ThreadLocal應定義為靜態成員變數,程式碼片段3中的定義方式是不提倡的。
2. 能通過傳值傳遞的參數,不要通過ThreadLocal儲存,以免造成ThreadLocal的濫用。
3. 線上程池的情況下,在ThreadLocal業務周期處理完成時,最好顯式的調用remove()方法,清空”線程局部變數”中的值。
4. 正常情況下使用ThreadLocal不會造成記憶體溢出,但如3.2中所述,弱引用的只是threadLocal,儲存的值依然是強引用的,如果threadLocal依然被其他對象強引用,”線程局部變數”是無法回收的。
以上是本人對ThreadLocal對象的一些瞭解,如有不足,還請指正。
JAVA ThreadLocal 對象 ServletActionContext