深刻理解ThreadLocal
用途
咱們通常用ThreadLocal來提供線程局部變量。線程局部變量會在每一個Thread內擁有一個副本,Thread只能訪問本身的那個副本。文字解釋老是晦澀的,咱們來看個例子。java
public class Test { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { Thread thread1 = new MyThread("lucy"); Thread thread2 = new MyThread("lily"); thread1.start(); thread2.start(); } private static class MyThread extends Thread { MyThread(String name) { super(name); } @Override public void run() { Thread thread = Thread.currentThread(); threadLocal.set("i am " + thread.getName()); try { //睡眠兩秒,確保線程lucy和線程lily都調用了threadLocal的set方法。 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(thread.getName() + " say: " + threadLocal.get()); } } }
這個例子很是簡單,就是建立了lucy和lily兩個線程。在線程內部,調用threadLocal的set方法存入一字符串,睡眠2秒後輸出線程名稱和threadLocal中的字符串。咱們運行這單代碼,看一下輸出內容。數組
lucy say: i am lucy lily say: i am lily
原理
上面例子很好的解釋了ThreadLocal的做用,接下來咱們分析一下這是如何實現的。異步
咱們定位到ThreadLocal的set方法。源碼中set方法被拆分爲幾個方法,爲了表述方便筆者將這幾個方法進行了整合。ide
public void set(T value) { //獲取當前線程 Thread t = Thread.currentThread(); //獲取當前線程的ThreadLocalMap ThreadLocalMap map = t.threadLocals; if (map != null) //將數據放入ThreadLocalMap中,key是當前ThreadLocal對象,值是咱們傳入的value。 map.set(this, value); else //初始化ThreadLocalMap,並以當前ThreadLocal對象爲Key,value爲值存入map中。 t.threadLocals = new ThreadLocalMap(this, value); }
經過上面這段代碼能夠看到,ThreadLocal的set方法主要是經過當前線程的ThreadLocalMap實現的。ThreadLocalMap是一個Map,它的key是ThreadLoacl,value是Object。
TreadLocal的get方法的源碼我就不貼出來了,大致上與set方法相似,就是先獲取到當前線程的ThreadLocalMap,而後以this爲key能夠取得value。
到這裏咱們基本上明白了ThreadLocal的工做原理,咱們總結一下this
每一個Thread實例內部都有一個ThreadLocalMap,ThreadLocalMap是一種Map,它的key是ThreadLocal,value是Object。
ThreadLocal的set方法實際上是往當前線程的ThreadLocalMap中存入數據,其key是當前ThreadLocal對象,value是set方法中傳入的值。
使用數據時,以當前ThreadLocal爲key,從當前線程的ThreadLocalMap中取出數據。線程
ThreadLocalMap
上面咱們介紹了ThreadLocal主要是經過線程的ThreadLocalMap實現的。設計
static class ThreadLocalMap { private ThreadLocal.ThreadLocalMap.Entry[] table; static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> var1, Object var2) { super(var1); this.value = var2; } } }
ThreadLocalMap是一種Map,其內部維護着一個Entry[]。code
ThreadLocalMap實際上是就是將Key和Value包裝成Entry,而後放入Entry數組中。咱們看一下它的set方法。對象
private void set(ThreadLocal<?> key, Object value) { 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) { //若是已經存在,直接替換value e.value = value; return; } if (k == null) {//若是當前位置的key ThreadLocal爲空,替換key和value。下文ThreadLocal內存分析中會提到爲何會有這段代碼。 replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value);//該位置沒有數據,直接存入。 int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) //檢查是否擴容 rehash(); } private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
到這裏,若是你瞭解HashMap,應該能夠看出ThreadLocalMap就是一種HashMap。不過它並無採用java.util.HashMap中數組+鏈表的方式解決Hash衝突,而是採用index後移的方式。
咱們簡單分析一下這段代碼:生命週期
經過ThreadLocal的threadLocalHashCode與當前Map的長度計算出數組下標 i。
從i開始遍歷Entry數組,這會有三種狀況:
Entry的key就是咱們要set的ThreadLocal,直接替換Entry中的value。
Entry的key爲空,直接替換key和value。
發生了Hash衝突,當前位置已經有了數據,查找下一個可用空間。
找到沒有數據的位置,將key和value放入。
檢查是否擴容。
咱們知道,HashMap是一種get、set都很是高效的集合,它的時間複雜度只有O(1)。可是若是存在嚴重的Hash衝突,那HashMap的效率就會下降不少。咱們經過上段代碼知道,ThreadLocalMap是經過 key.threadLocalHashCode & (len-1)計算Entry存放index的。len是當前Entry[]的長度,這沒什麼好說的。那看來祕密就在threadLocalHashCode中了。咱們來看一下threadLocalHashCode是如何產生的。
public class ThreadLocal<T> { private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } }
這段代碼很是簡單。有個全局的計數器nextHashCode,每有一個ThreadLocal產生這個計數器就會加0x61c88647,而後把當前值賦給threadLocalHashCode。
ThreadLocal內存分析
不知從什麼時候起,網上開始流傳ThreadLocal有內存泄漏的問題。下面咱們從ThreadLocal的內存入手,分析一下這種說法是否正確。話很少說直接上圖。
如今,咱們假設ThreadLocal完成了本身的使命,與ThreadLocalRef斷開了引用關係。此時內存圖變成了這樣。
系統GC發生時,因爲Heap中的ThreadLocal只有來自key的弱引用,所以ThreadLocal內存會被回收到。
到這裏,value被留在了Heap中,而咱們沒辦法經過引用訪問它。value這塊內存將會持續到線程結束。若是不想依賴線程的生命週期,那就調用remove方法來釋放value的內存吧。我的認爲,這種設計應該也是JDK開發大佬的無奈之舉。咱們從源碼中來感覺一下這些大佬爲了儘量下降內存泄漏風險做出的努力。
ThreadLocalMap.Entry軟引用ThreadLocal,避免了ThreadLocal的內存泄漏。
還記得ThreadLocalMap set方法中的這段代碼嗎?
private void set(ThreadLocal<?> key, Object value) { for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); ... if (k == null) { replaceStaleEntry(key, value, i); return; } } }
ThreadLocal get方法獲取時,有一段若是Entry的key爲null,移除Entry和Entry.value的代碼。
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; 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--; } ... } return i; }