簡介
成都創(chuàng)新互聯(lián)公司總部坐落于成都市區(qū),致力網(wǎng)站建設(shè)服務(wù)有成都網(wǎng)站設(shè)計、網(wǎng)站制作、網(wǎng)絡(luò)營銷策劃、網(wǎng)頁設(shè)計、網(wǎng)站維護、公眾號搭建、成都微信小程序、軟件開發(fā)等為企業(yè)提供一整套的信息化建設(shè)解決方案。創(chuàng)造真正意義上的網(wǎng)站建設(shè),為互聯(lián)網(wǎng)品牌在互動行銷領(lǐng)域創(chuàng)造價值而不懈努力!
ThreadLocal是每個線程自己維護的一個存儲對象的數(shù)據(jù)結(jié)構(gòu),線程間互不影響實現(xiàn)線程封閉。一般我們通過ThreadLocal對象的get/set方法存取對象。
源碼分析
ThreadLocal的set方法源碼如下
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // 根據(jù)當前線程獲得ThreadLocalMap對象 if (map != null) map.set(this, value); // 如果有則set else createMap(t, value); // 否則創(chuàng)建ThreadLocalMap對象 } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
通過getMap方法,可見我們返回的map實際上是Thread對象的threadLocals屬性。而這個ThreadLocalMap就是用來存儲數(shù)據(jù)的結(jié)構(gòu)。
ThreadLocalMap介紹
ThreadLocalMap是ThreadLocal的核心,定義在ThreadLocal類里的內(nèi)部類,他維護了一個Enrty數(shù)組。ThreadLocal存/取數(shù)據(jù)都是通過操作Enrty數(shù)組來實現(xiàn)的。
Enrty數(shù)組作為一個哈希表,將對象通過開放地址方法散列到這個數(shù)組中。作為對比,HashMap則是通過鏈表法將對象散列到數(shù)組中。
開放地址法就是元素散列到數(shù)組中的位置如果有沖突,再以某種規(guī)則在數(shù)組中找到下一個可以散列的位置,而在ThreadLocalMap中則是使用線性探測的方式向后依次查找可以散列的位置。
Enery介紹
Enery在這里我們稱之為元素,是散列表中維護的對象單元。
// 哈希映射表中的元素使用其引用字段作為鍵(它始終是ThreadLocal對象)繼承WeakReference。 // 注意,null鍵(即entry.get()== null)表示不再引用該鍵,因此可以從表中刪除該元素。 // 這些元素在下面的代碼中稱為“舊元素”。 // 這些“舊元素”就是臟對象,因為存在引用不會被GC, // 為避免內(nèi)存泄露需要代碼里清理,將引用置為null,那么這些對象之后就會被GC清理。 // 實際上后面的代碼很大程度上都是在描述如何清理“舊元素”的引用 static class Entry extends WeakReference> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
到這里可能有兩個疑問
1、既然要存儲的內(nèi)容是線程獨有的對象,為什么不直接在Thread里設(shè)置一個屬性直接存儲該對象?或者說為什么要維護一個Entry散列表來存儲內(nèi)容并以ThreadLocal對象作為key?
答:一個ThreadLocal對象只屬于一個線程,但一個線程可以實例化ThreadLocal對象。而ThreadLocalMap維護的數(shù)組存儲的就是以ThreadLocal實例作為key的Entry對象。
2、ThreadLocalMap中的Enery為什么要繼承WeakReference?
答:首先弱引用會在ThreadLocal對象不存在強引用的情況,弱引用對象會在下次GC時被清除。
將ThreadLocal對象作為弱引用目的是為了防止內(nèi)存泄露。
假設(shè)Enery的key不是弱引用,即使在我們的代碼里threadLocal引用已失效,threadLocal也不會被GC,因為當前線程持有ThreadLocalMap的引用,而ThreadLocalMap持有Entry數(shù)組的引用,Entry對象的key又持有threadLocal的引用,threadLocal對象針對當前線程可達,所以不會被GC。
而Enery的key值threadLocal作為弱引用,在引用失效時會被GC。但即使threadLocal做為弱引用被GC清理,Entry[]還是存在entry對象,只是key為null,vlue對象也還存在,這些都是臟對象。弱引用不單是清理了threadLocal對象,它的另一層含義是可以標識出Enery[]數(shù)組中哪些元素應(yīng)該被GC(我們這里稱為舊元素),然后程序里找出這些entry并清理。
ThreadLocalMap的set方法
回到前面提到的set方法,當map不為null時會調(diào)用ThreadLocalMap的set方法。
ThreadLocalMap的set方法描述了如何將值散列到哈希表中,是開放地址法以線性探測方式散列的實現(xiàn)。在成功set值之后,嘗試清理一些舊元素,如果沒有發(fā)現(xiàn)舊元素則判斷閾值,確認哈希表是否足夠大、是否需要擴容。如果哈希表過于擁擠,get/set值會發(fā)生頻繁的沖突,這是不期望的情況。ThreadLocalMap的set方法代碼及詳細注釋如下
private void set(ThreadLocal<?> key, Object value) { // We do not 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. // 我們不像get()那樣先使用快速路徑(直接散列)判斷 // 因為使用set()創(chuàng)建新元素至少與替換現(xiàn)有元素一樣頻繁,在這種情況下,散列后立刻判斷會容易失敗。 // 所以直接先線性探測 Entry[] tab = table; int len = tab.length; // 根據(jù)hashcode散列到數(shù)組位置 int i = key.threadLocalHashCode & (len-1); // 開放地址法處理散列沖突,線性探測找到可以存放位置 // 遍歷數(shù)組找到下一個可以存放元素的位置,這種位置包含三種情況 // 1.元素的key已存在,直接賦值value // 2.元素的key位null,說明k作為弱引用被GC清理,該位置為舊數(shù)據(jù),需要被替換 // 3.直到遍歷到一個數(shù)組位置為null的位置賦值 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) {//key已存在則直接更新 e.value = value; return; } if (k == null) { //e不為null但k為null說明k作為弱引用被GC,是舊數(shù)據(jù)需要被清理 // i為舊數(shù)據(jù)位置,清理該位置并依據(jù)key合理地散列或?qū)alue替換到數(shù)組中 // 然后重新散列i后面的元素,并順便清理i位置附近的其他舊元素 replaceStaleEntry(key, value, i); return; } } // 遍歷到一個數(shù)組位置為null的位置賦值 tab[i] = new Entry(key, value); int sz = ++size; // 調(diào)用cleanSomeSlots嘗試性發(fā)現(xiàn)并清理舊元素,如果沒有發(fā)現(xiàn)且舊元素當前容量超過閾值,則調(diào)用rehash if (!cleanSomeSlots(i, sz) && sz >= threshold) // 此時認為表空間不足,全量遍歷清理舊元素,清理后判斷容量若大于閾值的3/4,若是則擴容并從新散列 rehash(); }
replaceStaleEntry方法
replaceStaleEntry方法是當我們線性探測時,如果碰到了舊元素就執(zhí)行。該方法做的事情比較多,可以總結(jié)為我們在staleSlot位置發(fā)現(xiàn)舊元素,將新值覆蓋到staleSlot位置上并清理staleSlot附近的舊元素?!案浇敝傅氖莝taleSlot位置前后連續(xù)的非null元素。代碼及詳細注釋如下
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). // 向前檢查是否存在舊元素,一次性徹底清理由于GC清除的弱引用key導致的舊數(shù)據(jù),避免多次執(zhí)行 int slotToExpunge = staleSlot; // 向前遍歷找到entry不為空且key為null的位置賦值給slotToExpunge for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first // staleSlot位置向后遍歷如果位置不為空,判斷key是否已經(jīng)存在 // 回想前面我們是set實例的時候,碰到舊元素的情況下調(diào)用該方法,所以很可能在staleSlot后面key是已經(jīng)存在的 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. // 如果我們找到鍵,那么我們需要將它與舊元素交換以維護哈希表順序。 // 然后可以將交換后得到的舊索引位置 // 或其上方遇到的任何其他舊索引位置傳給expungeStaleEntry清理舊條 // 如果碰到key相同的值則覆蓋value if (k == key) { e.value = value; // i位置與staleSlot舊數(shù)據(jù)位置做交換,將數(shù)組元素位置規(guī)范化,維護哈希表順序 // 這里維護哈希表順序是必要的,舉例來說,回想前面threadLocal.set實例的判斷,是線性探測找到可以賦值的位置 // 如果哈希順序不維護,可能造成同一個實例被賦值多次的情況 // 包括后面清理舊元素的地方都要重新維護哈希表順序 tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists // 開始清理前面的舊元素 // 如果前面向前或向后查找的舊元素不存在,也就是slotToExpunge == staleSlot //此時slotToExpunge = i,此時位置i的元素是舊元素,需要被清理 // slotToExpunge用來存儲第一個需要被清理的舊元素位置 if (slotToExpunge == staleSlot) slotToExpunge = i; // 清理完slotToExpunge位置及其后面非空連續(xù)位置后,通過調(diào)用cleanSomeSlots嘗試性清理一些其他位置的舊元素 // cleanSomeSlots不保證清理全部舊元素,它的時間復雜度O(log2n),他只是全量清理舊元素或不清理的折中 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we do not find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. // 如果前面向前查找的舊元素不存在,也就是slotToExpunge == staleSlot,而此時位置i為舊元素,所以將i賦值給slotToExpunge // slotToExpunge用來存儲第一個需要被清理的舊元素位置 if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot // 如果向后遍歷非空entry都沒有找到key,則直接賦值給當前staleSlot舊元素位置 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them // 通過前面根據(jù)staleSlot向前/向后遍歷,如果發(fā)現(xiàn)有舊元素則清理 if (slotToExpunge != staleSlot) // 清理完slotToExpunge位置及其后面非空連續(xù)位置后,通過調(diào)用cleanSomeSlots嘗試性清理一些其他位置的舊元素 // cleanSomeSlots不保證清理全部舊元素,它的時間復雜度O(log2n),他只是全量清理舊元素或不清理的折中 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
expungeStaleEntry方法
查找到的舊元素都會執(zhí)行expungeStaleEntry方法。expungeStaleEntry頻繁被使用,它是清理舊元素的核心方法。該方法的做的事情就是:清理包括staleSlot位置后面連續(xù)為空元素中的所有舊元素并重新散列,返回staleSlot后面首個null位置。代碼及詳細注釋如下
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot // 清空staleSlot位置的元素 tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null // 舊位置清理后,后面的元素需要重新散列到數(shù)組里,直到遇到數(shù)組位置為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) { // k == null說明此位置也是舊數(shù)據(jù),需要清理 e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); // 將staleSlot后面不為空位置重新散列,如果與當前位置不同,則向前移動到h位置后面(包括h)的首個空位置 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; }
cleanSomeSlots方法
cleanSomeSlots是一個比較靈動的方法。就如他的名字"some"一樣。該方法只是嘗試性地尋找一些舊元素。添加新元素或替換舊元素時都會調(diào)用此方法。它的執(zhí)行復雜度log2(n),他是 “不清理”和“全量清理”的折中。若有發(fā)現(xiàn)舊元素返回true。代碼及詳細注釋如下
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); } // n >>>= 1無符號右移1位,即移動次數(shù)以n的二進制最高位的1的位置為基準 // 所以時間復雜度log2(n) } while ( (n >>>= 1) != 0); return removed; }
rehash/expungeStaleEntries/resize方法
在成功set值后,通過閾值判斷,如果程序認為表空間不足就會調(diào)用rehash方法。
rehash做了兩件事,首先全量遍歷清理舊元素,然后在清理后判斷容量是否足夠,若成立則2倍擴容并重新散列。
expungeStaleEntries則是全量清理舊元素,resize則是二倍擴容。
// rehash全量地遍歷清理舊元素,然后判斷容量若大于閾值的3/4,則擴容并從新散列 // 程序認為表空間不足時會調(diào)用該方法 private void rehash() { // 全量遍歷清理舊元素 expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis // 適當?shù)臄U容,以避免hash散列到數(shù)組時過多的位置沖突 if (size >= threshold - threshold / 4) // 2倍擴容并重新散列 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); } } // 二倍擴容 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; }
ThreadLocal的get方法
ThreadLocal的get邏輯相比set要簡單的多。他只是將threadLocal對象散列到數(shù)組中,通過線性探測的方式找到匹配的值。代碼及詳細注釋如下
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 如果map不為null初始化一個key為當前threadLocal值為null的ThreadLocalMap對象 return setInitialValue(); } private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else // 直接散列找不到的情況,調(diào)用getEntryAfterMiss線性探測查找期望元素 return getEntryAfterMiss(key, i, e); } 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); else i = nextIndex(i, len); e = tab[i]; } return null; }
remove方法
remove即將引用清空并調(diào)用清理舊元素方法。所以remove不會產(chǎn)生舊元素,當我們確認哪些內(nèi)容需要移除時優(yōu)先使用remove方法清理,盡量不要交給GC處理。避免get/set發(fā)現(xiàn)舊元素的情況過多。
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } 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; } } }
總結(jié)
ThreadLocal最大的復雜性在于如何處理舊元素,目的是為了避免內(nèi)存泄露。
在新增或替換元素成功后,為了盡可能少地在get/set時發(fā)現(xiàn)有舊元素的情況,在清理舊元素后多次調(diào)用cleanSomeSlots嘗試性地發(fā)現(xiàn)并清理一些舊元素,為了執(zhí)行效率,“cleanSome”是“no clean” 不清理和“clean all”全量清理之間一的種平衡。
expungeStaleEntry在清理自己位置上的舊元素的同時也會清理附近的舊元素,為得都是減少get/set發(fā)現(xiàn)舊元素的情況。即便如此,在哈希表容量過多時也會全量清理一遍舊元素并擴容。
當確認元素需要清除時,優(yōu)先使用remove方法。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。