ThreadLocal必知必會

前言面試

自從被各大互聯網公司的"造火箭"級面試難度吊打以後,痛定思痛,遂收拾心神,從基礎的知識點開始展開地毯式學習。每個非天才程序猿都有一個對35歲的恐懼,而消除恐懼最好的方式就是面對它、看清它、乃至跨過它,學習就是這個世界給普通人提供的一把成長型武器,掌握了它,便能與粗暴的生活一戰。數組

最近看了好幾篇有關ThreadLocal的面試題和技術博客,下面結合源碼本身作一個總結,以方便後面的自我回顧。安全

本文重點:多線程

一、ThreadLocal如何發揮做用的?性能

二、ThreadLocal設計的巧妙之處學習

三、ThreadLocal內存泄露問題this

四、如何讓新線程繼承原線程的ThreadLocal?spa

下面開始正文。線程

1、ThreadLocal如何發揮做用的?設計

首先來一段本地demo,工做中用的時候也是相似的套路,先聲明一個ThreadLocal,而後調用它的set方法將特定對象存入,不過用完以後必定別忘了加remove,此處是一個錯誤的示範...

 1 public class ThreadLocalDemo {
 2 
 3     private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
 4 
 5     public static void main(String[] args) {
 6         threadLocal.set("main thread");
 7         new Thread(() -> {
 8             threadLocal.set("thread");
 9         }).start();
10     }
11 }

追蹤一下set方法:

1     public void set(T value) {
2         Thread t = Thread.currentThread();
3         ThreadLocalMap map = getMap(t); // 一、獲得map
4         if (map != null)
5             map.set(this, value); // 二、放入value
6         else
7             createMap(t, value); // 三、初始化map
8     }

在threadLocal的set方法中有三個主要方法,第一個方法是去當前線程的threadLocals中獲取map,該map是Thread類的一個成員變量。

 

 若是線程是新建出來的,threadLocals這個值確定是null,此時會進入方法3 createMap中(以下)新建一個ThreadLocalMap,存入當前的ThreadLocal對象和value。

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

相對而言最複雜的是方法2 map.set()方法,以下,該方法代碼位於ThreadLocal的內部類ThreadLocalMap中。

 1 private void set(ThreadLocal<?> key, Object value) {
 2 
 3             // We don't use a fast path as with get() because it is at
 4             // least as common to use set() to create new entries as
 5             // it is to replace existing ones, in which case, a fast
 6             // path would fail more often than not.
 7 
 8             Entry[] tab = table;
 9             int len = tab.length;
10             int i = key.threadLocalHashCode & (len-1); // 一、獲取要存放的key的數組下標
11 
12             for (Entry e = tab[i];
13                  e != null;
14                  e = tab[i = nextIndex(i, len)]) { ///二、若是下標所在位置是空的,則直接跳過此for循環,不爲空則進入內部判斷邏輯,不然往下移動數組指針 ***
15                 ThreadLocal<?> k = e.get();
16                 // 2.1 若是不是空,則判斷key是否是原數組下標處Entry對象的key,是的話直接替換value便可
17                 if (k == key) {
18                     e.value = value;
19                     return;
20                 }
21                 // 2.2 若是數組下標處的Entry的key是null,說明弱引用已經被回收,此時也替換掉value ***
22                 if (k == null) {
23                     replaceStaleEntry(key, value, i);
24                     return;
25                 }
26             }
27             // 三、說明數組中i所在位置是空的,直接new一個Entry賦值
28             tab[i] = new Entry(key, value);
29             int sz = ++size;
30             if (!cleanSomeSlots(i, sz) && sz >= threshold) // 四、清理掉一些無用的數據 ***
31                 rehash();
32         }

該方法加了註釋,重要的地方均用 *** 標識了出來,雖然可能沒法清楚每一步的用意與原理,但大致作了什麼都能知道---在此方法中完成了value對象的存儲

寫到這裏的時候,BZ的思惟也不清晰了,趕忙畫個圖清醒下:

 

 完成set操做後,當前線程、threadLocal變量、ThreadLocal對象、ThreadLocalMap之間的關係基本梳理出來了。

插播一個擴展,補充一下引用相關的知識。Java中的強引用是除非代碼主動修改或者持有引用的變量被清理,不然該引用指向的對象必定不會被垃圾回收器回收;軟引用是隻要JVM內存空間夠用,就不會對該引用指向的對象進行垃圾回收;而弱引用是隻要進行垃圾回收時該對象只有弱引用,則就會被回收。

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         }

下面開始填坑。

 

2、ThreadLocal設計的巧妙之處

上面ThreadLocalMap.set方法的代碼中,標識了三顆星的第二步有什麼意義?

 答:找到第一個未被佔的下標位置。ThreadLocalMap中的Entry[]數組是一個環狀結構,經過nextIndex方法便可證實,當i+1比len大的時候,返回0即初始位置。當出現hash衝突時,HashMap是經過在下標位置串接鏈表來存放數據,而ThreadLocalMap不會有那麼大的訪問量,因此採用了更加輕便的解決hash衝突的方式-日後移一個位置,看看是否是空的,不是空的則繼續日後移,直到找到空的位置。

1 private static int nextIndex(int i, int len) {
2             return ((i + 1 < len) ? i + 1 : 0);
3         }

 

爲何編寫JDK代碼的大佬們要將Entry的key設置爲弱引用?標識了三顆星的2.2步爲何key會是null?

答:key設置爲弱引用是爲了當threadLocal被清理以後堆中的ThreadLocal對象也能被清理掉,避免ThreadLocal對象帶來的內存泄露。這也是key是null的緣由-當只有key這個弱引用指向ThreadLocal對象時,發生一次垃圾回收就會將該ThreadLocal回收了。但這種方式無法徹底避免內存泄露,由於回看以前的內存分佈圖,key指向的對象雖然被釋放了內存,可是value還在啊,並且因爲這個value對應的key是null,也就不會有地方使用這個value,完蛋,內存釋放不了了。

這時2.2的邏輯就發揮一部分做用了,若是當前i下標的key是null,說明已經被回收了,那麼直接把這個位置佔用就好了,反正已經沒人用了。

 

標識了三顆星的第四步 cleanSomeSlots方法的職責是什麼?

 答:該方法用於清除部分key爲null的Entry對象。爲何是清除部分呢?且看方法實現:

 1 private boolean cleanSomeSlots(int i, int n) {
 2             boolean removed = false;
 3             Entry[] tab = table;
 4             int len = tab.length;
 5             do {
 6                 i = nextIndex(i, len);
 7                 Entry e = tab[i];
 8                 if (e != null && e.get() == null) {
 9                     n = len;
10                     removed = true;
11                     i = expungeStaleEntry(i);
12                 }
13             } while ( (n >>>= 1) != 0);
14             return removed;
15         }

在do/while循環中,每次循環給n右移一位(傳入的n是數組中存放的數據個數),若是遇到一個key爲null的狀況, 說明數組中可能存在多個這種對象,因此將n置爲整個數組的長度,多循環幾回,而且調用了expungeStaleEntry方法將key爲null的value引用去掉。cleanSomeSlots方法沒有采用徹底循環遍歷的方式,主要出於方法執行效率的考量。

下面再詳細說說expungeStaleEntry方法的邏輯,該方法專門用於清除key爲null的這種過時數據,並且還附帶一個做用:將以前由於hash衝突致使下標後移的對象收縮緊湊一些,提升遍歷查詢效率。

 1 private int expungeStaleEntry(int staleSlot) {
 2             Entry[] tab = table;
 3             int len = tab.length;
 4             // 一、清除入參所在下標的value
 5             // expunge entry at staleSlot
 6             tab[staleSlot].value = null;
 7             tab[staleSlot] = null;
 8             size--;
 9             // 二、從入參下標開始日後遍歷,一直遍歷到tab[i]等於null的位置中止
10             // Rehash until we encounter null
11             Entry e;
12             int i;
13             for (i = nextIndex(staleSlot, len);
14                  (e = tab[i]) != null;
15                  i = nextIndex(i, len)) {
16                 ThreadLocal<?> k = e.get();
17                 if (k == null) { // 2.1 若是key爲null,找的就是這種渾水摸魚的,必除之然後快
18                     e.value = null;
19                     tab[i] = null;
20                     size--;
21                 } else {
22                     int h = k.threadLocalHashCode & (len - 1);
23                     if (h != i) { // 2.2 h即當前這個entry的key應該在的下標位置,若是跟i不一樣,說明這個entry是發生下標衝突後移過來的
24                         tab[i] = null; // 此時要將如今處於i位置的e移到h位置,故先將tab[i]置爲null,在後面再將tab[i]位置的e存入h位置
25 
26                         // Unlike Knuth 6.4 Algorithm R, we must scan until
27                         // null because multiple entries could have been stale.
28                         while (tab[h] != null) // 2.3 這裏經過while循環來找到h以及後面第一個爲null的下標位置,這個位置就是存放e的位置
29                             h = nextIndex(h, len);
30                         tab[h] = e;
31                     }
32                 }
33             }
34             return i;
35         }

 

爲何存放線程相關的變量要這樣設計?爲什麼不能在ThreadLocal中定義一個Map的成員變量,key就是線程,value就是要存放的對象,這樣設計豈不是更簡潔易懂?

答:這樣設計能作到訪問效率和空間佔用的最優。先看訪問效率,若是採用日常思惟的方式用一個公共Map來存放key-value,則當多線程訪問的時候確定會有訪問衝突,即便使用ConcurrentHashMap也一樣會有鎖競爭帶來的性能消耗,而如今這種將map存入Thread中的設計,則保證了一個線程只能訪問本身的map,而且是單線程確定不會有線程安全問題,簡直不要太爽。

 

3、ThreadLocal內存泄露問題

文章開頭的示例中,用static修飾了ThreadLocal,這樣作是否必要?有什麼做用?

答:用static修飾ThreadLocal變量,使得在整個線程執行過程當中,Map中的key不會被回收(由於有一個靜態變量的強引用在引用着呢),因此想何時取就何時取,並且從頭至尾都是同一個threadLocal變量(再new一個除外),存入map中時也只佔用一個下標位置,不會出現不可控的內存佔用超限。因而可知,設置爲static並非徹底必要,但做用是有的。

ThreadLocal中針對key爲null的狀況,在好幾處用不一樣的姿式進行清除,就是爲了不內存泄漏,這樣是否能徹底避免內存泄漏?若不能,如何作才能徹底避免?

答:能最大程度的避免內存泄漏,但不能徹底避免。線程執行完了就會將ThreadLocalMap內存釋放,但若是是線程池中的線程,一直重複利用,那麼它的Map中的value數據就可能越攢越多得不到釋放引發內存泄露。如何避免?用完後在finally中調一下remove方法吧,前輩大佬們都給寫好了的方法,且用便可。

 

另外,threadLocal變量不能是局部變量,由於key是弱引用,若是設置成局部變量,則方法執行完以後強引用清除只剩弱引用,就可能被釋放掉,key變爲null,這樣也就背離了ThreadLocal在同一個線程通過多個方法時共享同一個變量的設計初衷。

 

4、如何讓新線程繼承原線程的ThreadLocal?

 答:new一個InheritableThreadLocal對象set數據便可,這時會存入當前Thread的成員變量 inheritableThreadLocals中。當在當前線程中new一個新線程時,在新線程的init方法中會將當前線程的inheritableThreadLocals存入新線程中,完成數據的繼承。

 

 Old Thread(ZZQ):畢生功力都傳授給你了,還不趕忙去爲禍人間?

New Thread(Pipe River): ...

相關文章
相關標籤/搜索