JAVA ThreadLocal對象淺析

最近在開發過程中,在做一個字典項服務的時候,最開始採用了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,不會有任何問題。以下爲”線程局部變量”的存儲圖:

tlocal

“線程局部變量”的存儲圖

由於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對象的一些瞭解,如有不足,還請指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章