1、ThreadLocal 線程私有的。java
爲何說ThreadLocal是線程私有的? 上源碼 ThreadLocal.set()。多線程
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
從源碼得知。咱們在往 ThreadLocal 中存數據時。 它首先 獲取到當前線程。並從當前線程中拿到 ThreadLocalMap 。再往裏存放數據。jvm
接下來咱們再來看ThreadLocalMap 中 key ,value 究竟又存放的是什麼。顯然 ThreadLocalMap 中 key ->ThreadLocal ;value ->咱們set 進去的 value。this
private void set(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); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
由源碼,咱們很天然就能得出結論。 既然調用 ThreadLocal .set() 方法時,每次都是獲取當前線程,再完成數據存儲動做的。那麼,它天然 具有了線程隔離性。由某一個線程所持有。因此ThreadLocal是線程私有的。spa
接下來,咱們用代碼來證實.net
/** * 首先 我先初始化一個 threadLocal 。、 * 再主線程main 中 新啓一個線程 thread,並在該線程中完成 set操做。 * 由此 當前應用存在兩個線程 一個是 main ,一個是 thread * 因爲threadLocal 是線程私有的。因此 主線程 main 獲取的值爲 null */ public class Test { public static void main(String[] args) { ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); Thread thread = new Thread(()->{ System.out.println(String.format("往 線程%s 中 存入 1 ",Thread.currentThread().getName())); threadLocal.set(1); try{ Thread.sleep(1000); System.out.println(String.format("線程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get())); }catch(Exception ex){ ex.printStackTrace(); } }); thread.start(); try{ // 讓 主線程睡一下子。確保 cpu 已執行 thread 中的 run()方法 Thread.sleep(500); }catch(Exception ex){ ex.printStackTrace(); } System.out.println(String.format("線程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get())); } }
驗證完畢。讓咱們繼續來看下源碼。線程
1.1 首先,咱們來看一下 Thread 類。java源碼中包含一個 ThreadLocalMap 的成員變量 threadLocalsdebug
/*java源碼中包含一個 ThreadLocalMap 的成員變量 threadLocals */ /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
從上圖咱們能夠看出ThreadLocalMap 是 ThreadLocal 中的 靜態內部類。code
1.2 接着咱們來看下 ThreadLocal 中的源碼。orm
static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
看過源碼後咱們發現 ThreadLocalMap 中 也包含一個 靜態內部類 Entry 繼承自 WeakReference<ThreadLocal<?>> 同時擁有 一個ThreadLocal 的引用。和 value。
那麼問題來了。此處爲何要用 弱引用?
首先。咱們再來複習一下,弱引用的特色。 當一個對象沒有被強引用存在時。 弱引用 將被jvm忽視。直接gc掉。想一下,若是不是 弱引用。而是強引用,會有什麼問題。假使 Entry 未繼承弱引用,則 entry 對 ThreadLocal 強引用。 則 當 ThreadLocal 被複製爲null時 意味着,ThreadLocal 已經沒有用了。 但 entry 對 ThreadLocal 的 強引用。致使 ThreadLocal 沒有辦法被回收。 會形成內存泄漏。 因此 這裏被繼承自 弱引用。
驗證來了。上代碼,看看 ThreadLocal 被置爲空後。 是否被gc了。
/** * 首先 我建立了兩個 ThreadLocal 對象 分別是 threadLocal,threadLocal_2 * 分別往 主線程 main 中 存放數字 1,2 * 而後將 threadLocal 置爲空。 而後再 手動 gc 。 * 分別再gc 前 和 gc 後 查看 當前 線程中的 ThreadLocalMap。 * 因爲獲取 ThreadLocalMap 訪問級別爲default 因此我這裏將用 debug的方式進行查看。 */ public class Test1 { public static void main(String[] args) { ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); ThreadLocal<Integer> threadLocal_2 = new ThreadLocal<>(); threadLocal.set(1); threadLocal_2.set(2); System.out.println(String.format("線程 %s;中的threadLocal值:%s", Thread.currentThread().getName(),threadLocal.get())); threadLocal_2.get(); threadLocal = null; System.gc(); threadLocal_2.get(); } }
未gc前。debug 看到的 狀況。
gc 後。 debug 看到的狀況。
終上所訴。驗證了 弱引用的實際效果。 和 爲何不用強引用 的緣由。
這裏,咱們發現了。 弱引用後, ThreadLocalMap 中對應的 entry 的 referent (指向 threadLocal的引用)確實被gc了。
繼續看源碼。 referent 是哪裏來的。 entry 繼承自 WeakReference<ThreadLocal<?>> 。而 WeakReference 又繼承自 抽象類 Reference<T>。
private T referent; /* Treated specially by GC */ volatile ReferenceQueue<? super T> queue; /* When active: NULL * pending: this * Enqueued: next reference in queue (or this if last) * Inactive: this */ @SuppressWarnings("rawtypes") volatile Reference next;
好,瞭解到這裏。 咱們不難發現。 雖然被gc了。可是 仍是存在一個問題,ThreadLocalMap -》entry -》key -》 referent (threadLocal)已然爲null了。可是卻仍然存在ThreadLocalMap中,佔用的部分的內存。 應用不可能經過某個引用再次拿到 entry 中的value了。 那不就是內存泄漏了嗎?
好問題! 這裏編寫人員,也想到了該問題。因此,他們的處理邏輯是:再調用 set,remove 方法時,調用方法 expungeStaleEntry 將 鍵爲null 的對象remove掉。
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 將entry的value賦值爲null,這樣方便GC時將真正value佔用的內存給釋放出來;將entry賦值爲null,size減1,這樣這個slot就又能夠從新存放新的entry了 // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; // 從staleSlot後一個index開始向後遍歷,直到遇到爲null的entry for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // 若是entry的key爲null,則清除掉該entry if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { // key的hash值不等於目前的index,說明該entry是由於有哈希衝突致使向後移動到當前index位置的 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) // 對該entry,從新進行hash並解決衝突 h = nextIndex(h, len); tab[h] = e; } } } // 返回通過整理後的,位於staleSlot位置後的第一個爲null的entry的index值 return i; }
到這裏,這樣處理,就萬事大吉了嗎? 遠遠沒有這麼簡單。 新問題又來了。 假使 set完後,就再也沒有調用 set 和 remove 方法。那 不仍是內存泄漏了嗎?
因此再使用 ThreadLocal 時,要養成一個好習慣,ThreadLocal 再沒有用時,就將 ThreadLocal 置爲null。以避免出現內存泄漏。
最後,咱們來分析一個問題。ThreadLocal 是線程私有的。若是是多線程中(多線程中的線程是可複用的)使用了 ThreadLocal 會有什麼問題?
答案也很簡單。若是沒有及時清理 ThreadLocal 除內存泄露外,還可能引起數據問題。 話很少說, 上代碼。
/** * 前後建立6個任務,前3個線程寫數據,後3個線程讀取數據。 * * 發現threadLocal 裏的 值被取出了。 * * 假使有個業務場景是 往當前線程 存放 用戶名(採用ThreadLocal來存儲) 。 * 先進行非空判斷,再進行 存儲。 結果,悲劇了。 上個線程的 ThreadLocal 並無清理。致使 不爲空。 * 結果上線文中存儲的用戶名就亂套了。可能就張冠李戴了 */ public class Test2 { public static void main(String[] args) { ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); ThreadLocal<String> threadLocal = new ThreadLocal<String>(); for(int i=0;i<3;i++){ fixedThreadPool.execute(()->{ try { threadLocal.set("ckr"); } catch (Exception e) { e.printStackTrace(); } }); } try{ Thread.sleep(2000); }catch(Exception ex){ } for(int i=0;i<3;i++){ fixedThreadPool.execute(()->{ try { System.out.println(threadLocal.get()); } catch (Exception e) { e.printStackTrace(); } }); } try{ Thread.sleep(2000); }catch(Exception ex){ } fixedThreadPool.shutdown(); } }
以上。關於ThreadLocal的一些問題,咱們都瞭解了,再次,特別強調,ThreadLocal 有風險。須要謹慎使用。
關於 java 中的四種引用強,軟,弱,虛。 請查看上篇博客: http://www.javashuo.com/article/p-czdxdtqo-nx.html