ThreadLocal

參考:http://www.iteye.com/topic/103804
http://www.iteye.com/topic/777716

源碼分析

  爲了解釋ThreadLocal類的工作原理,必須同時介紹與其工作甚密的其他幾個類

  • ThreadLocalMap(內部類)
  • Thread

  首先,在Thread類中有一行:

    /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

  其中ThreadLocalMap類的定義是在ThreadLocal類中,真正的引用卻是在Thread類中。同時,ThreadLocalMap中用於存儲數據的entry定義:

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

  從中我們可以發現這個Map的key是ThreadLocal類的實例對象,value爲用戶的值,並不是網上大多數的例子key是線程的名字或者標識。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) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

其中的getMap方法:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

給當前Thread類對象初始化ThreadlocalMap屬性:

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

  到這裏,我們就可以理解ThreadLocal究竟是如何工作的了

  1. Thread類中有一個成員變量屬於ThreadLocalMap類(一個定義在ThreadLocal類中的內部類),它是一個Map,他的key是ThreadLocal實例對象。
  2. 當爲ThreadLocal類的對象set值時,首先獲得當前線程的ThreadLocalMap類屬性,然後以ThreadLocal類的對象爲key,設定value。get值時則類似。
  3. ThreadLocal變量的活動範圍爲某線程,是該線程“專有的,獨自霸佔”的,對該變量的所有操作均由該線程完成!也就是說,ThreadLocal 不是用來解決共享對象的多線程訪問的競爭問題的,因爲ThreadLocal.set() 到線程中的對象是該線程自己使用的對象,其他線程是不需要訪問的,也訪問不到的。當線程終止後,這些值會作爲垃圾回收。
  4. 由ThreadLocal的工作原理決定了:每個線程獨自擁有一個變量,並非是共享的,下面給出一個例子:
public class Son implements Cloneable{
    public static void main(String[] args){
        Son p=new Son();
        System.out.println(p);
        Thread t = new Thread(new Runnable(){  
            public void run(){
                ThreadLocal<Son> threadLocal = new ThreadLocal<>();
                System.out.println(threadLocal);
                threadLocal.set(p);
                System.out.println(threadLocal.get());
                threadLocal.remove();
                try {
                    threadLocal.set((Son) p.clone());
                    System.out.println(threadLocal.get());
                } catch (CloneNotSupportedException e) {
                    e.printStackTrace();
                }
                System.out.println(threadLocal);
            }}); 
        t.start();
    }
}

輸出:

Son@7852e922
java.lang.ThreadLocal@3ffc8195
Son@7852e922
Son@313b781a
java.lang.ThreadLocal@3ffc8195

也就是如果把一個共享的對象直接保存到ThreadLocal中,那麼多個線程的ThreadLocal.get()取得的還是這個共享對象本身,還是有併發訪問問題。 所以要在保存到ThreadLocal之前,通過克隆或者new來創建新的對象,然後再進行保存。
  ThreadLocal的作用是提供線程內的局部變量,這種變量在線程的生命週期內起作用。作用:提供一個線程內公共變量(比如本次請求的用戶信息),減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的複雜度,或者爲線程提供一個私有的變量副本,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產生影響。

如何實現一個線程多個ThreadLocal對象,每一個ThreadLocal對象是如何區分的呢?
查看源碼,可以看到:

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
      return nextHashCode.getAndAdd(HASH_INCREMENT);
}

  對於每一個ThreadLocal對象,都有一個final修飾的int型的threadLocalHashCode不可變屬性,對於基本數據類型,可以認爲它在初始化後就不可以進行修改,所以可以唯一確定一個ThreadLocal對象。
  但是如何保證兩個同時實例化的ThreadLocal對象有不同的threadLocalHashCode屬性:在ThreadLocal類中,還包含了一個static修飾的AtomicInteger([əˈtɒmɪk]提供原子操作的Integer類)成員變量(即類變量)和一個static final修飾的常量(作爲兩個相鄰nextHashCode的差值)。由於nextHashCode是類變量,所以每一次調用ThreadLocal類都可以保證nextHashCode被更新到新的值,並且下一次調用ThreadLocal類這個被更新的值仍然可用,同時AtomicInteger保證了nextHashCode自增的原子性。

爲什麼不直接用線程id來作爲ThreadLocalMap的key?
  這一點很容易理解,因爲直接用線程id來作爲ThreadLocalMap的key,無法區分放入ThreadLocalMap中的多個value。比如我們放入了兩個字符串,你如何知道我要取出來的是哪一個字符串呢?
  而使用ThreadLocal作爲key就不一樣了,由於每一個ThreadLocal對象都可以由threadLocalHashCode屬性唯一區分或者說每一個ThreadLocal對象都可以由這個對象的名字唯一區分(下面的例子),所以可以用不同的ThreadLocal作爲key,區分不同的value,方便存取。

public class Son implements Cloneable{
    public static void main(String[] args){
        Thread t = new Thread(new Runnable(){  
            public void run(){
                ThreadLocal<Son> threadLocal1 = new ThreadLocal<>();
                threadLocal1.set(new Son());
                System.out.println(threadLocal1.get());
                ThreadLocal<Son> threadLocal2 = new ThreadLocal<>();
                threadLocal2.set(new Son());
                System.out.println(threadLocal2.get());
            }}); 
        t.start();
    }
}

ThreadLocal的內存泄露問題
  根據上面Entry方法的源碼,我們知道ThreadLocalMap是使用ThreadLocal的弱引用作爲Key的。下圖是本文介紹到的一些對象之間的引用關係圖,實線表示強引用,虛線表示弱引用:

  如上圖,ThreadLocalMap使用ThreadLocal的弱引用作爲key,如果一個ThreadLocal沒有外部強引用引用他,那麼系統gc的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key爲null的Entry,就沒有辦法訪問這些key爲null的Entry的value,如果當前線程再遲遲不結束的話,這些key爲null的Entry的value就會一直存在一條強引用鏈:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠無法回收,造成內存泄露。
  
  ThreadLocalMap設計時的對上面問題的對策:
ThreadLocalMap的getEntry函數的流程大概爲:

  1. 首先從ThreadLocal的直接索引位置(通過ThreadLocal.threadLocalHashCode & (table.length-1)運算得到)獲取Entry e,如果e不爲null並且key相同則返回e;
  2. 如果e爲null或者key不一致則向下一個位置查詢,如果下一個位置的key和當前需要查詢的key相等,則返回對應的Entry。否則,如果key值爲null,則擦除該位置的Entry,並繼續向下一個位置查詢。在這個過程中遇到的key爲null的Entry都會被擦除,那麼Entry內的value也就沒有強引用鏈,自然會被回收。仔細研究代碼可以發現,set操作也有類似的思想,將key爲null的這些Entry都刪除,防止內存泄露。
      但是光這樣還是不夠的,上面的設計思路依賴一個前提條件:要調用ThreadLocalMap的getEntry函數或者set函數。這當然是不可能任何情況都成立的,所以很多情況下需要使用者手動調用ThreadLocal的remove函數,手動刪除不再需要的ThreadLocal,防止內存泄露。所以JDK建議將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命週期就更長,由於一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然後remove它,防止內存泄露。

關於ThreadLocalMap內部類的簡單介紹
  初始容量16,負載因子2/3,解決衝突的方法是再hash法,也就是:在當前hash的基礎上再自增一個常量。

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