轉:https://www.cnblogs.com/hama1993/p/10382523.html
項目中我們如果想要某個對象在程序運行中的任意位置獲取到,就需要藉助ThreadLocal來實現,這個對象稱作線程的本地變量,下面就介紹下ThreadLocal是如何做到線程內本地變量傳遞的,
一、基本使用
先來看下基本用法:
private static ThreadLocal tl = new ThreadLocal<>(); public static void main(String[] args) throws Exception { tl.set(1); System.out.println(String.format("當前線程名稱: %s, main方法內獲取線程內數據爲: %s", Thread.currentThread().getName(), tl.get())); fc(); new Thread(ThreadLocalTest::fc).start(); } private static void fc() { System.out.println(String.format("當前線程名稱: %s, fc方法內獲取線程內數據爲: %s", Thread.currentThread().getName(), tl.get())); }
運行結果:
當前線程名稱: main, main方法內獲取線程內數據爲: 1 當前線程名稱: main, fc方法內獲取線程內數據爲: 1 當前線程名稱: Thread-0, fc方法內獲取線程內數據爲: null
可以看到,main線程內任意地方都可以通過ThreadLocal獲取到當前線程內被設置進去的值,而被異步出去的fc調用,卻由於替換了執行線程,而拿不到任何數據值,那麼我們現在再來改造下上述代碼,在異步發生之前,給Thread-0線程也設置一個上下文數據:
private static ThreadLocal tl = new ThreadLocal<>(); public static void main(String[] args) throws Exception { tl.set(1); System.out.println(String.format("當前線程名稱: %s, main方法內獲取線程內數據爲: %s", Thread.currentThread().getName(), tl.get())); fc(); new Thread(()->{ tl.set(2); //在子線程裏設置上下文內容爲2 fc(); }).start(); Thread.sleep(1000L); //保證下面fc執行一定在上面異步代碼之後執行 fc(); //繼續在主線程內執行,驗證上面那一步是否對主線程上下文內容造成影響 } private static void fc() { System.out.println(String.format("當前線程名稱: %s, fc方法內獲取線程內數據爲: %s", Thread.currentThread().getName(), tl.get())); }
運行結果爲:
當前線程名稱: main, main方法內獲取線程內數據爲: 1
當前線程名稱: main, fc方法內獲取線程內數據爲: 1
當前線程名稱: Thread-0, fc方法內獲取線程內數據爲: 2
當前線程名稱: main, fc方法內獲取線程內數據爲: 1
可以看到,主線程和子線程都可以獲取到自己的那份上下文裏的內容,而且互不影響。
二、原理分析
ok,上面通過一個簡單的例子,我們可以瞭解到ThreadLocal(以下簡稱TL)具體的用法,這裏先不討論它實質上能給我們帶來什麼好處,先看看其實現原理,等這些差不多瞭解完了,我再通過我曾經做過的一個項目,去說明TL的作用以及在企業級項目裏的用處。
我以前在不瞭解TL的時候,想着如果讓自己實現一個這種功能的輪子,自己會怎麼做,那時候的想法很單純,覺得通過一個Map就可以解決,Map的key設置爲Thread.currentThread(),value設置爲當前線程的本地變量即可,但後來想想就覺得不太現實了,實際項目中可能存在大量的異步線程,對於內存的開銷是不可估量的,而且還有個嚴重的問題,線程是運行結束後就銷燬的,如果按照上述的實現方案,map內是一直持有這個線程的引用的,導致明明執行結束的線程對象不能被jvm回收,造成內存泄漏,時間久了,會直接OOM。
所以,java裏的實現肯定不是這麼簡單的,下面,就來看看java裏的具體實現吧。
先來了解下,TL的基本實現,爲了避免上述中出現的問題,TL實際上是把我們設置進去的值以k-v的方式放到了每個Thread對象內(TL對象做k,設置的值做v),也就是說,TL對象僅僅起到一個標記、對Thread對象維護的map賦值的作用。
先從set方法看起:
public void set(T value) { Thread t = Thread.currentThread(); //獲取當前線程 ThreadLocal.ThreadLocalMap map = getMap(t); //獲取到當前線程持有的ThreadLocalMap對象 if (map != null) map.set(this, value); //直接set值,具體方法在下面 else createMap(t, value); // 爲空就給當前線程創建一個ThreadLocalMap對象,賦值給Thread對象,具體方法在下面 } ThreadLocal.ThreadLocalMap getMap(Thread t) { return t.threadLocals; //每個線程都有一個ThreadLocalMap,key爲TL對象(其實是根據對象hash計算出來的值),value爲該線程在此TL對象下存儲的內容值 } private void set(ThreadLocal<?> key, Object value) { ThreadLocal.ThreadLocalMap.Entry[] tab = table; //獲取存儲k-v對象的數組(散列表) int len = tab.length; int i = key.threadLocalHashCode & (len-1); //根據TL對象的hashCode(也是特殊計算出來的,保證每個TL對象的hashCode不同)計算出下標 for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //線性探查法解決哈希衝突問題,發現下標i已經有Entry了,則就查看i+1位置處是否有值,以此類推 ThreadLocal<?> k = e.get(); //獲取k if (k == key) { //若k就是當前TL對象,則直接爲其value賦值 e.value = value; return; } if (k == null) { //若k爲空,則認爲是可回收的Entry,則利用當前k和value組成新的Entry替換掉該可回收Entry replaceStaleEntry(key, value, i); return; } } //for循環執行完沒有終止程序,說明遇到了空槽,這個時候直接new對象賦值即可 tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) //這裏用來清理掉k爲null的廢棄Entry rehash(); //如果沒有發生清除Entry並且size超過閾值(閾值 = 最大長度 * 2/3),則進行擴容 } //直接爲當前Thread初始化它的ThreadLocalMap對象 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue); } ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY]; //初始化數組 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //計算初始位置 table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue); //因爲初始化不存在hash衝突,直接new size = 1; setThreshold(INITIAL_CAPACITY); //給閾值賦值,上面已經提及,閾值 = 最大長度 * 2/3 }
通過上述代碼,我們大致瞭解了TL在set值的時候發生的一些操作,結合之前說的,我們可以確定的是,TL其實對於線程來說,只是一個標識,而真正線程的本地變量被保存在每個線程對象的ThreadLocalMap裏,這個map裏維護着一個Entry[]的數組(散列表),Entry是個k-v結構的對象(如圖1-1),k爲TL對象,v爲對應TL保存在該線程內的本地變量值,值得注意的是,這裏的k針對TL對象的引用是個弱引用,來看下源碼:
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
爲什麼這裏需要弱引用呢?我們先來看一張圖,結合上面的介紹和這張圖,來了解TL和Thread間的關係:
圖1-1
圖中虛線表示弱引用,那麼爲什麼要這麼做呢?
簡單來說,一個TL對象被創建出來,並且被一個線程放到自己的ThreadLocalMap裏,假如TL對象失去原有的強引用,但是該線程還沒有死亡,如果k不是弱引用,那麼就意味着TL並不能被回收,現在k爲弱引用,那麼在TL失去強引用的時候,gc可以直接回收掉它,弱引用失效,這就是上面代碼裏會進行檢查,k=null的清除釋放內存的原因(這個可以參考下面expungeStaleEntry方法,而且set、get、remove都會調用該方法,這也是TL防止內存泄漏所做的處理)。
綜上,簡單來說這個弱引用就是用來解決由於使用TL不當導致的內存泄漏問題的,假如沒有弱引用,那麼你又用到了線程池(池化後線程不會被銷燬),然後TL對象又是局部的,那麼就會導致線程池內線程裏的ThreadLocalMap存在大量的無意義的TL對象引用,造成過多無意義的Entry對象,因爲即便調用了set、get等方法檢查k=null,也沒有作用,這就導致了內存泄漏,長時間這樣最終可能導致OOM,所以TL的開發者爲了解決這種問題,就將ThreadLocalMap裏對TL對象的引用改爲弱引用,一旦TL對象失去強引用,TL對象就會被回收,那麼這裏的弱引用指向的值就爲null,結合上面說的,調用操作方法時會檢查k=null的Entry進行回收,從而避免了內存泄漏的可能性。
因爲TL解決了內存泄漏的問題,因此即便是局部變量的TL對象且啓用線程池技術,也比較難造成內存泄漏的問題,而且我們經常使用的場景就像一開始的示例代碼一樣,會初始化一個全局的static的TL對象,這就意味着該對象在程序運行期間都不會存在強引用消失的情況,我們可以利用不同的TL對象給不同的Thread裏的ThreadLocalMap賦值,通常會set值(覆蓋原有值),因此在使用線程池的時候也不會造成問題,異步開始之前set值,用完以後remove,TL對象可以多次得到使用,啓用線程池的情況下如果不這樣做,很可能業務邏輯也會出問題(一個線程存在之前執行程序時遺留下來的本地變量,一旦這個線程被再次利用,get時就會拿到之前的髒值);
說完了set,我們再來看下get:
public T get() { Thread t = Thread.currentThread(); ThreadLocal.ThreadLocalMap map = getMap(t); //獲取線程內的ThreadLocalMap對象 if (map != null) { ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this); //根據當前TL對象(key)獲取對應的Entry if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; //直接返回value即可 } } return setInitialValue(); //如果發現當前線程還沒有ThreadLocalMap對象,則進行初始化 } private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); //計算下標 ThreadLocal.ThreadLocalMap.Entry e = table[i]; if (e != null && e.get() == key) //根據下標獲取的Entry對象如果key也等於當前TL對象,則直接返回結果即可 return e; else return getEntryAfterMiss(key, i, e); //上面說過,有些情況下存在下標衝突的問題,TL是通過線性探查法來解決的,所以這裏也一樣,如果上面沒找到,則繼續通過下標累加的方式繼續尋找 } private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) { ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); //繼續累加下標的方式一點點的往下找 if (k == key) //找到了就返回出去結果 return e; if (k == null) //這裏也會檢查k==null的Entry,滿足就執行刪除操作 expungeStaleEntry(i); else //否則繼續累加下標查找 i = nextIndex(i, len); e = tab[i]; } return null; //找不到返回null } //這裏也放一下nextIndex方法 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
最後再來看看remove方法:
public void remove() { ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); //清除掉當前線程ThreadLocalMap裏以當前TL對象爲key的Entry } private void remove(ThreadLocal<?> key) { ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //計算下標 for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { //找到目標Entry e.clear(); //清除弱引用 expungeStaleEntry(i); //通過該方法將自己清除 return; } } } private int expungeStaleEntry(int staleSlot) { //參數爲目標下標 ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; tab[staleSlot].value = null; //首先將目標value清除 tab[staleSlot] = null; size--; // Rehash until we encounter null ThreadLocal.ThreadLocalMap.Entry e; int i; // 由目標下標開始往後逐個檢查,k==null的清除掉,不等於null的要進行rehash for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
目前主要方法set、get、remove已經介紹完了,包含其內部存在的弱引用的作用,以及實際項目中建議的用法,以及爲什麼要這樣用,也進行了簡要的說明,下面一篇會進行介紹InheritableThreadLocal的用法以及其原理性分析。