一.ThreadLoacl的理解:
官方的講:
ThreadLocal是一個本地線程副本變量工具類,主要用於將私有線程和該線程存放的副本對象做一個映射,各個線程之間的變量互不干擾
通俗的講:
ThreadLocal也叫做線程本地變量,ThreadLoacl為變量在每個線程中的都創建了副本,每個線程可以訪問自己內部的副本變量,線程之間互不影響
二.TreadLocal的原理:
從上圖我們可以初步窺見ThreadLocal的核心機制:
1)每個Thread線程內部都有一個Map
2)Map里面儲存線程本地對象key和線程的變量副本value
3)Thread內部的Map是由ThreadLocal維護的,由ThreadLocal負責向Map獲取和設置線程的變量值
這樣對於不同的線程,每次獲取副本值時,別的線程並不能獲取到當前線程的副本值,這樣就形成了副本隔離,互不干擾
三.ThreadLocal的底層源碼
ThreadLocal類提供了以下幾個核心方法:
1.get方法:獲取當前線程的副本變量值
2.set方法:設置當前線程的副本變量值
3.remove方法:移除當前線程的副本變量值
4.initilaValue方法:初始化當前線程的副本變量值,初始化null
1)ThreadLocal.get():
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } protected T initialValue() { return null; }
源碼解析:
1.獲取當前線程的ThreadLocalMap對象threadLocals(實際儲存副本值的Map)
2.Map不為空的話,從Map中獲取線程儲存的K-V Entry結點,然后從Entry結點中獲取Value副本值返回
3.Map為空的話,返回初始值null,之后還需向Map中添加value為null的鍵值對,避免空指針異常
2.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); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
源碼解析:
1.獲取當前線程的成員變量Map
2.Map不為空:重新將ThreadLocal對象和Value副本放入Map中
3.Map為空:對線程成員變量ThreadLocalMap進行初始化創建,並將ThreadLocal對象和Value副本放入Map中
3.ThreadLocal.remove():
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
源碼分析:
直接調用了ThreadLocalMap的remove方法(后面我們還會探究ThreadLocalMap類的底層源碼!,這里先放着)
4.ThreadLocal.initialValue() :
protected T initialValue() { return null; }
就是直接返回null
小結一下:我們發現ThreadLocal的底層源碼都有一個ThreadLocalMap類,那么ThreadLocalMap類的底層源碼又是什么樣子的呢?我們一起來看看吧!
四.ThreadLocalMap的底層源碼分析
ThreadLocalMap是ThreadLocal內部的一個Map實現,然而它沒有實現任何集合的接口規范,因為它僅供ThreadLocal內部使用,數據結構采用數組+開方地址法,Entry繼承WeakRefrence,是基於ThreadLocal這種特殊場景實現的Map,它的實現方式很值得我們取研究!!
1.ThreadLocalMap中Entry的源碼
static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } }
源碼分析:
1.Entry中key只能是ThreadLocal對象,被規定死了的
2.Entry繼承了WeakRefrence(弱引用,生存周期只能活到下次GC前),但是只有Key是弱引用,Value並不是弱引用
ps:value既然不是弱引用,那么key在被回收之后(key=null)Value並沒有被回收,如果當前線程被回收了那還好,這樣value也和線程一起被回收了,要是當前線程是線程池這樣的環境,線程結束沒有銷毀回收,那么Value永遠不會被回收,當存在大量這樣的value的時候,就會產生內存泄漏,那么Java 8中如何解決這個問題的呢?
解決辦法:
以上是ThreadLocalMap的set方法,for循環遍歷整個Entry數組,遇到key=null的就會替換,這樣就不存在value內存泄漏的問題了!!!
2.ThreaLocalMap中key的HashCode計算
ThreaLocalMap的key是ThreaLocal,它不會傳統的調用ThreadLocal的hashcode方法(繼承自object的hashcode),而是調用nexthashcode,源碼如下:
private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); //1640531527 這是一個神奇的數字,能夠讓hash槽位分布相當均勻 private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
源碼分析:
我們發現ThreaLocalMap的hashcode計算沒有采用模長度的方法,沒有采用拉鏈法,采用的是開放地址法,其槽位采用靜態的AtomicInteger每次增加1640531527實現,沖突了則加1或者減1繼續進行增加1640531527
我們把這個數叫做魔數,通過這個魔數我們可以位key產生完美的槽位分配,hahs沖突的次數很少
(據說魔數和黃金比例,斐波那契數列存在某種關系)
3.ThreaLocalMap中set方法:
private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); // 用key的hashCode計算槽位 // hash沖突時,使用開放地址法 // 因為獨特和hash算法,導致hash沖突很少,一般不會走進這個for循環 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { // key 相同,則覆蓋value e.value = value; return; } if (k == null) { // key = null,說明 key 已經被回收了,進入替換方法 replaceStaleEntry(key, value, i); return; } } // 新增 Entry tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些過期的值,並判斷是否需要擴容 rehash(); // 擴容 }
源碼分析:
1.先是計算槽位
2.Entry數組中存在需要插入的key,直接替換即可,存在key=null,也是替換(可以避免value內存泄漏)
3.Entry數組中不存在需要插入的key,也沒有key=null,新增一個Entry,然后判斷一下需不需要擴容和清除過期的值(關於擴容和清除過期值先不細講)
4.ThreadLocalMap中getEntry方法:
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) // 無hash沖突情況 return e; else return getEntryAfterMiss(key, i, e); // 有hash沖突情況 }
源碼分析:
1.計算槽位i,判斷table[i]是否有目標key,沒有(hahs沖突了)則進入getEntryAfterMiss方法
getEntryAfterMiss方法分析:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); // 清除過期的slot else i = nextIndex(i, len); e = tab[i]; } return null; }
源碼分析:
遇到hash沖突之后繼續向后查找,並且會在查找路上清除過期的slot
5.ThreadLocalMap中rehash方法:
private void rehash() { expungeStaleEntries(); // 清除過程中,size會減小,在此處重新計算是否需要擴容 // 並沒有直接使用threshold,而是用較低的threshold (約 threshold 的 3/4)提前觸發resize if (size >= threshold - threshold / 4) resize(); } private void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { Entry e = tab[j]; if (e != null && e.get() == null) expungeStaleEntry(j); } }
源碼分析:
先調用expungeStaleEntries()清除所有過期的slot,然后提前觸發resize(約 threshold 的 3/4的時候)
下面看看resize():
private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
擴容2倍,同時在Entry移動過程中會清除一些過期的entry
6.ThreadLocal中的remove方法:
private void remove(ThreadLocal<?> key) { 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)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
源碼分析:
遍歷Entry數組尋找需要刪除的ThreadLocal,建議在ThreadLocal使用完成之后再調用此方法
現在再詳細分析一下ThreadLocalMap的set方法中的幾個方法:
1.replaceStaleEntry方法:替換
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // 往前尋找過期的slot int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // 找到 key 或者 直到 遇到null 的slot 才終止循環 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // 如果找到了key,那么需要將它與過期的 slot 交換來維護哈希表的順序。 // 然后可以將新過期的 slot 或其上面遇到的任何其他過期的 slot // 給 expungeStaleEntry 以清除或 rehash 這個 run 中的所有其他entries。 if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // 如果存在,則開始清除前面過期的entry if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // 如果我們沒有在向前掃描中找到過期的條目, // 那么在掃描 key 時看到的第一個過期 entry 是仍然存在於 run 中的條目。 if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // 如果沒有找到 key,那么在 slot 中創建新entry tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // 如果還有其他過期的entries存在 run 中,則清除他們 if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
上文中run的意思不好翻譯,理解為開放地址中一個slot中前后不為null的連續entry
2.cleanSomeSlots方法:清除一些slot(按照規則清除“一些”slot,而不是全部)
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); // 清除方法 } } while ( (n >>>= 1) != 0); // n = n / 2, 對數控制循環 return removed; }
當新元素被添加時,或者另外一個過期元素已經被刪除的時候,會調用該方法,該方法會試探性的掃描一些Entry尋找過期的條目,它執行對數數量的掃描,是一種基於不掃描(快速但保留垃圾)和所有元素掃描之間的平衡!!
對數數量的掃描!!!
這是一種折中的方案
3.expungeStaleEntry:清除
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 清除當前過期的slot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash 直到 null 的 slot 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--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
真正的清除,不僅會清除當前過期的slot,還會繼續往后查詢直到遇到null的slot為止,對於查詢遍歷中沒有被回收的情況,做了一次rehash
推薦大佬的博客:https://www.cnblogs.com/micrari/p/6790229.html
寫的太詳細了,太強了,源碼注釋賊多
---------------------------------------------------------------------------------------------
沒有下文了,這是底線…