1. 程式人生 > >ThreadLocal 應用原理解析與常見問題

ThreadLocal 應用原理解析與常見問題

ThreadLocal是大家比較常用到的,在多執行緒下儲存執行緒相關資料十分合適。可是很多時候我們並沒有深入去了解它的原理。

首選提出幾個問題,稍後再針對這些問題一一解答。

  1. 提到ThreadLocal,大家常說ThreadLocal是弱引用,那麼ThreadLocal究竟是如何實現弱引用的呢?
  2. ThreadLocal是如何做到可以當做執行緒區域性變數的呢?
  3. 大家建立ThreadLocal變數時,為什麼都要用static修飾?
  4. 大家爭論不止的ThreadLocal記憶體洩漏是什麼鬼?

進入正題,先簡單瞭解下ThreadLocal 和 Thread,ThreadLocal的類結構:

 

 可以看到,ThreadLocal有個內部類ThreadLocalMap,ThreadLocalMap又有個內部類Entry。

Thread類有這樣一段原始碼:

class Thread implements Runnable {

    ...省略若干程式碼

    /* 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原始碼我們瞭解到,Thread持有的物件是ThreadLocal的ThreadLocalMap,這一點特別重要,執行緒相關資料都是通過ThreadLocalMap儲存的,而不是ThreadLocal。

此時我們得到的結論如下圖所示:

Thread的threadLocals屬性直接關聯的ThreadLocal.ThreadLocalMap,和ThreadLocal沒有絲毫關係

 

那麼ThreadLocal是做什麼的呢?其實ThreadLocal可以看做執行緒操作ThreadLocalMap的工具類,ThreadLocal暴漏了兩個公共方法get()和set(T)用來獲取和設定ThreadLocalMap。

瞭解一下set方法原始碼:

 

1     public void set(T value) {
2         Thread t = Thread.currentThread();
3         ThreadLocalMap map = getMap(t);
4         if (map != null)
5             map.set(this, value);
6         else
7             createMap(t, value);
8     }

 

 

 

 

 

 

 從原始碼第五行我們可以得到兩個重要的資訊:

  • 獲取ThreadLocalMap時,使用了當前Thread物件 t 作為引數。

    getMap(t)方法的實現很簡單:

      

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

 

    它返回的是Thread的 threadLocals 屬性,程式碼上驗證了:“執行緒區域性變數”是儲存在Thread物件的threadLocals屬性中,和 ThreadLocal 本身沒什麼關係。ThreadLocal 可以當做訪問的工具類。

    這裡我們第2個問題:ThreadLocal是如何做到可以當做執行緒區域性變數的已經有答案啦,所有的操作其實都是對Thread 下 threadLocals 的操作,所以跨執行緒操作也不會產生問題的,因為getMap()永遠返回當前執行緒的threadLocals屬性。

 

  • ThreadLocalMap是一個類似Map鍵值對的結構,此處傳入的key是固定值this,這個this不是執行緒物件喲,是當前的ThreadLocal物件,value即我們傳入的引數。

    小夥伴們是不是很奇怪為什麼要把this當做key呢?這就扯到我們文章開頭的第一個問題了:弱引用!

    跟進map.set(this, value);原始碼一看究竟:

 1         private void set(ThreadLocal<?> key, Object value) {
 2 
 3             Entry[] tab = table;
 4             int len = tab.length;
 5             int i = key.threadLocalHashCode & (len-1);
 6 
 7             for (Entry e = tab[i];
 8                  e != null;
 9                  e = tab[i = nextIndex(i, len)]) {
10                 ThreadLocal<?> k = e.get();
11 
12                 if (k == key) {
13                     e.value = value;
14                     return;
15                 }
16 
17                 if (k == null) {
18                     replaceStaleEntry(key, value, i);
19                     return;
20                 }
21             }
22 
23             tab[i] = new Entry(key, value);
24             int sz = ++size;
25             if (!cleanSomeSlots(i, sz) && sz >= threshold)
26                 rehash();
27         }

 

  檢視23行Entry的構造方法:

1         static class Entry extends WeakReference<ThreadLocal<?>> {
2             /** The value associated with this ThreadLocal. */
3             Object value;
4 
5             Entry(ThreadLocal<?> k, Object v) {
6                 super(k);
7                 value = v;
8             }
9         }

 

    Entry只有一個構造方法,該構造方法接受兩個引數k和v,k就是當前ThreadLocal物件,v是我要儲存的執行緒相關資料。通過上述程式碼標紅部分我們可以瞭解到對 k 使用了弱引用,但是value不是,value是強引用。至此第一個問題已經真相了,大家所說的ThreadLocal弱引用其實是ThreadLocalMap和ThreadLocal是弱引用關係。

    為什麼要這麼設計呢?

    首選我們整理下當前引用關係如下圖:

    

 

 

     value一般是執行緒相關的資料,執行緒回收後value -> null,強引用就不存在了。但是ThreadLocal物件的生命週期不一定和執行緒相關,可能執行緒消亡後ThreadLocal物件仍然被其它執行緒引用,如果使用強引用的話,ThreadLocalMap物件就無法釋放記憶體,發生記憶體洩漏的情況。使用弱引用就安全的多了,發生gc時弱引用指向的物件會被記憶體回收。

 

問題1和2已經在上文中提到,繼續看問題3,建立ThreadLocal物件時為什麼要用static修飾呢?

  個人感覺是基於兩點的考慮:

  • 第一是避免重複建立ThreadLocal物件,使用同一個ThreadLocal物件和多個ThreadLocal物件對程式碼本身沒什麼影響,實在沒必要重複建立多個物件。
  • 延長ThreadLocal的生命週期,方便使用。

   網上很多地方把static和記憶體洩漏聯絡起來,原諒我沒看出來這兩者有什麼關係。

    

最後來到第四個問題,也大家都關心的記憶體洩漏啦,。

  通過上面的引用關係圖我們瞭解到存在兩個引用關係,分別是key的弱引用和value的強引用。弱引用首選不可能導致記憶體洩漏,因為gc發生時弱引用的物件就有可能被回收了。所以。。。記憶體洩漏發生在強引用這個關係上。

  因為現線上程切換的開銷比較大,大家現在普遍使用執行緒池的技術去避免執行緒的頻繁建立。線上程池中,執行緒不會消亡,會被重複使用,so。。。。上邊的強引用得不到釋放了,記憶體洩漏就這樣發生了。其實我在JDK8上看到的是java已經為此做了一些工作了,比如執行下次set操作時遍歷key是null的Entry物件並釋放value的引用。雖然java本身做了一些工作,仍然強烈建議使用完ThreadLocal執行remove方法主動消除引用關係。

  文章結束了,如有紕漏,歡迎指出。

&n