最近在開發過程中,在做一個字典項服務的時候,最開始採用了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[]>();
public ThreadLocalDomail(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對象的一些瞭解,如有不足,還請指正。