1. 程式人生 > >深入JDK原始碼_Index --> 深入JDK原始碼之ThreadLocal類 --> 陶邦仁 又發現一牛人

深入JDK原始碼_Index --> 深入JDK原始碼之ThreadLocal類 --> 陶邦仁 又發現一牛人

深入JDK原始碼


http://my.oschina.net/xianggao/blog/392440

ThreadLocal概述

學習JDK中的類,首先看下JDK API對此類的描述,描述如下: 該類提供了執行緒區域性 (thread-local) 變數。這些變數不同於它們的普通對應物,因為訪問某個變數(通過其 get 或 set 方法)的每個執行緒都有自己的區域性變數,它獨立於變數的初始化副本。ThreadLocal其實就是一個工具類,用來操作執行緒區域性變數,ThreadLocal 例項通常是類中的 private static 欄位。它們希望將狀態與某一個執行緒(例如,使用者 ID 或事務 ID)相關聯。 例如,以下類生成對每個執行緒唯一的區域性識別符號。執行緒 ID 是在第一次呼叫UniqueThreadIdGenerator.getCurrentThreadId()

時分配的,在後續呼叫中不會更改。

其實ThreadLocal並非是一個執行緒的本地實現版本,它並不是一個Thread,而是threadlocalvariable(執行緒區域性變數)。也許把它命名為ThreadLocalVar更加合適。執行緒區域性變數(ThreadLocal)其實的功用非常簡單,就是為每一個使用該變數的執行緒都提供一個變數值的副本,是Java中一種較為特殊的執行緒繫結機制,是每一個執行緒都可以獨立地改變自己的副本,而不會和其它執行緒的副本衝突。

import java.util.concurrent.atomic.AtomicInteger;

public classUniqueThreadIdGenerator
{ private static final AtomicInteger uniqueId = new AtomicInteger(0); private static final ThreadLocal < Integer > uniqueNum = new ThreadLocal < Integer > () { @Override protected Integer initialValue(){ return uniqueId.getAndIncrement(); } }; public
staticintgetCurrentThreadId()
{ return uniqueId.get(); } }

從執行緒的角度看,每個執行緒都保持對其執行緒區域性變數副本的隱式引用,只要執行緒是活動的並且 ThreadLocal 例項是可訪問的;線上程消失之後,其執行緒區域性例項的所有副本都會被垃圾回收(除非存在對這些副本的其他引用)。

API表達了下面幾種觀點:

  1. ThreadLocal不是執行緒,是執行緒的一個變數,你可以先簡單理解為執行緒類的屬性變數。
  2. ThreadLocal 在類中通常定義為靜態類變數。
  3. 每個執行緒有自己的一個ThreadLocal,它是變數的一個‘拷貝’,修改它不影響其他執行緒。

既然定義為類變數,為何為每個執行緒維護一個副本(姑且成為‘拷貝’容易理解),讓每個執行緒獨立訪問?多執行緒程式設計的經驗告訴我們,對於執行緒共享資源(你可以理解為屬性),資源是否被所有執行緒共享,也就是說這個資源被一個執行緒修改是否影響另一個執行緒的執行,如果影響我們需要使用synchronized同步,讓執行緒順序訪問。

ThreadLocal適用於資源共享但不需要維護狀態的情況,也就是一個執行緒對資源的修改,不影響另一個執行緒的執行;這種設計是空間換時間,synchronized順序執行是時間換取空間

ThreadLocal介紹

從字面上來理解ThreadLocal,感覺就是相當於執行緒本地的。我們都知道,每個執行緒在jvm的虛擬機器裡都分配有自己獨立的空間,執行緒之間對於本地的空間是相互隔離的。那麼ThreadLocal就應該是該執行緒空間裡本地可以訪問的資料了。ThreadLocal變數高效地為每個使用它的執行緒提供單獨的執行緒區域性變數值的副本。每個執行緒只能看到與自己相聯絡的值,而不知道別的執行緒可能正在使用或修改它們自己的副本。

很多人看到這裡會容易產生一種錯誤的印象,感覺是不是這個ThreadLocal物件建立了一個類似於全域性的map,然後每個執行緒作為map的key來存取對應執行緒本地的value。你看,每個執行緒不一樣,所以他們對映到map中的key應該也不一樣。實際上,如果我們後面詳細分析ThreadLocal的程式碼時,會發現不是這樣的。它具體是怎麼實現的呢? 在此輸入圖片描述

ThreadLocal原始碼

ThreadLocal類本身定義了有get(), set()和initialValue()三個方法。前面兩個方法是public的,initialValue()是protected的,主要用於我們在定義ThreadLocal物件的時候根據需要來重寫。這樣我們初始化這麼一個物件在裡面設定它的初始值時就用到這個方法。ThreadLocal變數因為本身定位為要被多個執行緒來訪問,它通常被定義為static變數

ThreadLocal有一個ThreadLocalMap靜態內部類,你可以簡單理解為一個MAP,這個Map為每個執行緒複製一個變數的‘拷貝’儲存其中。

當執行緒呼叫ThreadLocal.get()方法獲取變數時,首先獲取當前執行緒引用,以此為key去獲取響應的ThreadLocalMap,如果此‘Map’不存在則初始化一個,否則返回其中的變數,程式碼如下:

 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();
 }

呼叫get方法如果此Map不存在首先初始化,建立此map,將執行緒為key,初始化的vlaue存入其中,注意此處的initialValue,我們可以覆蓋此方法,在首次呼叫時初始化一個適當的值。setInitialValue程式碼如下:

    private T setInitialValue(){
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        elsecreateMap(t, value);
        return value;
    }

set方法相對比較簡單如果理解以上倆個方法,獲取當前執行緒的引用,從map中獲取該執行緒對應的map,如果map存在更新快取值,否則建立並存儲,程式碼如下:

    publicvoidset(T value){
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        elsecreateMap(t, value);
    }

對於ThreadLocal在何處儲存變數副本,我們看getMap方法:獲取的是當前執行緒的ThreadLocal型別的threadLocals屬性。顯然變數副本儲存在每一個執行緒中。

/**
 * 獲取執行緒的ThreadLocalMap 屬性例項
 */
ThreadLocalMap getMap(Thread t){
        return t.threadLocals;
}

上面我們知道變數副本存放於何處,這裡我們簡單說下如何被java的垃圾收集機制收集,當我們不在使用時呼叫set(null),此時不在將引用指向該‘map’,而執行緒退出時會執行資源回收操作,將申請的資源進行回收,其實就是將屬性的引用設定為null。這時已經不在有任何引用指向該map,故而會被垃圾收集。

注意:如果ThreadLocal.set()進去的東西本來就是多個執行緒共享的同一個物件,那麼多個執行緒的ThreadLocal.get()取得的還是這個共享物件本身,還是有併發訪問問題。

看到ThreadLocal類中的變數只有這3個int型:

    /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode = new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

而作為ThreadLocal例項的變數只有 threadLocalHashCode 這一個,nextHashCode 和HASH_INCREMENT 是ThreadLocal類的靜態變數,實際上HASH_INCREMENT是一個常量,表示了連續分配的兩個ThreadLocal例項的threadLocalHashCode值的增量,而nextHashCode 的表示了即將分配的下一個ThreadLocal例項的threadLocalHashCode 的值。

現在來看看它的雜湊策略。所有ThreadLocal物件共享一個AtomicInteger物件nextHashCode用於計算hashcode,一個新物件產生時它的hashcode就確定了,演算法是從0開始,以HASH_INCREMENT = 0x61c88647為間隔遞增,這是ThreadLocal唯一需要同步的地方。根據hashcode定位桶的演算法是將其與陣列長度-1進行與操作:key.threadLocalHashCode & (table.length - 1)

0x61c88647這個魔數是怎麼確定的呢? ThreadLocalMap的初始長度為16,每次擴容都增長為原來的2倍,即它的長度始終是2的n次方,上述演算法中使用0x61c88647可以讓hash的結果在2的n次方內儘可能均勻分佈,減少衝突的概率。

可以來看一下建立一個ThreadLocal例項即new ThreadLocal()時做了哪些操作,從上面看到建構函式ThreadLocal()裡什麼操作都沒有,唯一的操作是這句:

    private final int threadLocalHashCode = nextHashCode();

那麼nextHashCode()做了什麼呢:

    privatestaticsynchronizedintnextHashCode(){
        int h = nextHashCode;
        nextHashCode = h + HASH_INCREMENT;
        return h;
    }

就是將ThreadLocal類的下一個hashCode值即nextHashCode的值賦給例項的threadLocalHashCode,然後nextHashCode的值增加HASH_INCREMENT這個值。

因此ThreadLocal例項的變數只有這個threadLocalHashCode,而且是final的,用來區分不同的ThreadLocal例項,ThreadLocal類主要是作為工具類來使用,那麼ThreadLocal.set()進去的物件是放在哪兒的呢?

看一下上面的set()方法,兩句合併一下成為:

        ThreadLocalMap map = Thread.currentThread().threadLocals;

這個ThreadLocalMap 類是ThreadLocal中定義的內部類,但是它的例項卻用在Thread類中:

    public classThreadimplementsRunnable{
        ......

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

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

這就說明了其實每個Thread本身就包含了兩個ThreadLocalMap物件的引用。這一點非常重要。以後每個thread要訪問他們的local物件時,就是訪問存在這個ThreadLocalMap裡的value。

ThreadLocalMap是定義在ThreadLocal類內部的私有類,它是採用“開放定址法”解決衝突的hashmap。key是ThreadLocal物件。當呼叫某個ThreadLocal物件的get或put方法時,首先會從當前執行緒中取出ThreadLocalMap,然後查詢對應的value:

public T get(){
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);     //拿到當前執行緒的ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);    // 以該ThreadLocal物件為key取value
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}
ThreadLocalMap getMap(Thread t){
    return t.threadLocals;
}

再看這句:

        if (map != null)
            map.set(this, value);

也就是將該ThreadLocal例項作為key,要保持的物件作為值,設定到當前執行緒的ThreadLocalMap 中,get()方法同樣大家看了程式碼也就明白了,ThreadLocalMap 類的程式碼太多了,我就不帖了,自己去看原始碼吧。

自然想法實現

一個非常自然想法是用一個執行緒安全的 Map<Thread,Object> 實現:

classThreadLocal{ 
  private Map values = Collections.synchronizedMap(new HashMap());

  public Object get(){
    Thread curThread = Thread.currentThread();
    Object o = values.get(curThread);
    if (o == null && !values.containsKey(curThread)) {
      o = initialValue();
      values.put(curThread, o);
    }
    return o;
  }

  publicvoidset(Object newValue){
    values.put(Thread.currentThread(), newValue);
  }
}

但這是非常naive的:

  1. ThreadLocal本意是避免併發,用一個全域性Map顯然違背了這一初衷;
  2. 用Thread當key,除非手動呼叫remove,否則即使執行緒退出了會導致:1)該Thread物件無法回收;2)該執行緒在所有ThreadLocal中對應的value也無法回收。

JDK 的實現剛好是反過來的: 在此輸入圖片描述

碰撞解決與神奇的0x61c88647

既然ThreadLocal用map就避免不了衝突的產生。

碰撞避免和解決 這裡碰撞其實有兩種型別: (1)只有一個ThreadLocal例項的時候(上面推薦的做法),當向thread-local變數中設定多個值的時產生的碰撞,碰撞解決是通過開放定址法, 且是線性探測(linear-probe)。 (2)多個ThreadLocal例項的時候,最極端的是每個執行緒都new一個ThreadLocal例項,此時利用特殊的雜湊碼0x61c88647大大降低碰撞的機率, 同時利用開放定址法處理碰撞。

神奇的0x61c88647 注意 0x61c88647 的利用主要是為了多個ThreadLocal例項的情況下用的。從ThreadLocal原始碼中找出這個雜湊碼所在的地方:

/**
 * ThreadLocals rely on per-thread linear-probe hash maps attached
 * to each thread (Thread.threadLocals and inheritableThreadLocals).
 * The ThreadLocal objects act as keys, searched via threadLocalHashCode.
 * This is a custom hash code (useful only within ThreadLocalMaps) that
 * eliminates collisions in the common case where consecutively
 * constructed ThreadLocals are used by the same threads,
 * while remaining well-behaved in less common cases.
 */
private final int threadLocalHashCode = nextHashCode();

/**
 * The next hash code to be given out. Updated atomically.
 * Starts at zero.
 */
private static AtomicInteger nextHashCode = new AtomicInteger();

/**
 * The difference between successively generated hash codes - turns
 * implicit sequential thread-local IDs into near-optimally spread
 * multiplicative hash values for power-of-two-sized tables.
 */
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * Returns the next hash code.
 */
privatestaticintnextHashCode(){
    return nextHashCode.getAndAdd(HASH_INCREMENT); 
}

注意例項變數threadLocalHashCode, 每當建立ThreadLocal例項時這個值都會累加 0x61c88647, 目的在上面的註釋中已經寫的很清楚了:為了讓雜湊碼能均勻的分佈在2的N次方的數組裡, 即 Entry[] table

下面來看一下ThreadLocal怎麼使用的這個 threadLocalHashCode 雜湊碼的,下面是ThreadLocalMap靜態內部類中的set方法的部分程式碼:

// Set the value associated with key.
privatevoidset(ThreadLocal key, Object value){

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i]; e != null;
      e = tab[i = nextIndex(i, len)]) {...}

    ...

key.threadLocalHashCode & (len-1)這麼用是什麼意思? 先看一下table陣列的長度吧:

/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
 */
private Entry[] table;

哇,ThreadLocalMap 中 Entry[] table 的大小必須是2的N次方呀(len = 2^N),那 len-1 的二進位制表示就是低位連續的N個1, 那 key.threadLocalHashCode & (len-1) 的值就是 threadLocalHashCode 的低N位, 這樣就能均勻的產生均勻的分佈? 我用python做個實驗吧:

>>> HASH_INCREMENT = 0x61c88647
>>> def magic_hash(n):
...     for i in range(n):
...         nextHashCode = i * HASH_INCREMENT + HASH_INCREMENT
...         print nextHashCode & (n - 1),
...     print
... 
>>> magic_hash(16)
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
>>> magic_hash(32)
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0

產生的雜湊碼分佈真的是很均勻,而且沒有任何衝突啊, 太神奇了。

ThreadLocal記憶體洩漏

很多人認為:threadlocal裡面使用了一個存在弱引用的map,當釋放掉threadlocal的強引用以後,map裡面的value卻沒有被回收.而這塊value永遠不會被訪問到了. 所以存在著記憶體洩露. 最好的做法是將呼叫threadlocal的remove方法。

說的也比較正確,當value不再使用的時候,呼叫remove的確是很好的做法.但記憶體洩露一說卻不正確. 這是threadlocal的設計的不得已而為之的問題. 首先,讓我們看看在threadlocal的生命週期中,都存在哪些引用吧. 看下圖: 實線代表強引用,虛線代表弱引用。 在此輸入圖片描述

每個thread中都存在一個map, map的型別是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal例項. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當把threadlocal例項tl置為null以後,沒有任何強引用指向threadlocal例項,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連線過來的強引用. 只有當前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收。通過原始碼看下此處的實現,如下:

        publicvoidset(T value){
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value); // 將當前threadLocal例項作為key
            elsecreateMap(t, value);
        }

        privatevoidset(ThreadLocal key, Object value){

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value); // 構造key-value例項
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

        static classEntryextendsWeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k); // 構造key弱引用
                value = v;
            }
        }

        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();
        }

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

從中可以看出,弱引用只存在於key上,所以key會被回收. 而value還存在著強引用.只有thead退出以後,value的強引用鏈條才會斷掉。一旦某個ThreadLocal物件沒有強引用了,它在所有執行緒內部的ThreadLocalMap中的key都將被GC掉(此時value還未回收),在map後續的get/set中會探測到key被回收的entry,將其 value 設定為 null 以幫助GC,因此 value 在 key 被 GC 後可能還會存活一段時間,但最終也會被回收。這個過程和java.util.WeakHashMap的實現幾乎是一樣的。

因此ThreadLocal本身是沒有記憶體洩露問題的,通常由它引發的記憶體洩露問題都是執行緒只 put 而忘了 remove 導致的,從上面分析可知,即使執行緒退出了,只要 ThreadLocal 還有強引用,該執行緒曾經 put 過的東西是不會被回收掉的。

ThreadLocal有何用

很多時候我們會建立一些靜態域來儲存全域性物件,那麼這個物件就可能被任意執行緒訪問到,如果它是執行緒安全的,這當然沒什麼說的。然而大部分情況下它不是執行緒安全的(或者無法保證它是執行緒安全的),尤其是當這個物件的類是由我們自己(或身邊的同事)建立的(很多開發人員對執行緒的知識都是一知半解,更何況執行緒安全)。

這時候我們就需要為每個執行緒都建立一個物件的副本。我們當然可以用ConcurrentMap<Thread, Object>來儲存這些物件,但問題是當一個執行緒結束的時候我們如何刪除這個執行緒的物件副本呢?

ThreadLocal為我們做了一切。首先我們宣告一個全域性的ThreadLocal物件(final static,沒錯,我很喜歡final),當我們建立一個新執行緒並呼叫threadLocal.get時,threadLocal會呼叫initialValue方法初始化一個物件並返回,以後無論何時我們在這個執行緒中呼叫get方法,都將得到同一個物件(除非期間set過)。而如果我們在另一個執行緒中呼叫get,將的到另一個物件,而且始終會得到這個物件。

當一個執行緒結束了,ThreadLocal就會釋放跟這個執行緒關聯的物件,這不需要我們關心,反正一切都悄悄地發生了。

(以上敘述只關乎執行緒,而不關乎get和set是在哪個方法中呼叫的。以前有很多不理解執行緒的同學總是問我這個方法是哪個執行緒,那個方法是哪個執行緒,我不知如何回答。)

所以,儲存”執行緒區域性變數”的map並非是ThreadLocal的成員變數, 而是java.lang.Thread的成員變數。也就是說,執行緒結束的時候,該map的資源也同時被回收。通過如下程式碼:

ThreadLocal的set,get方法中均通過如下方式獲取Map:
ThreadLocalMap map = getMap(t);

而getMap方法的程式碼如下:
ThreadLocalMap getMap(Thread t){
    return t.threadLocals;
}