前言面試
自從被各大互聯網公司的"造火箭"級面試難度吊打以後,痛定思痛,遂收拾心神,從基礎的知識點開始展開地毯式學習。每個非天才程序猿都有一個對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): ...