ThreadLocal
簡介 (簡要說明ThreadLocal
的做用)ThreadLocal實
現原理(說明ThreadLocal
的經常使用方法和原理)ThreadLocalMap
的實現 (說明核心數據結構ThreadLocalMap
的實現)先貼一段官方的文檔解釋算法
/** * This class provides thread-local variables. These variables differ from * their normal counterparts in that each thread that accesses one (via its * {@code get} or {@code set} method) has its own, independently initialized * copy of the variable. {@code ThreadLocal} instances are typically private * static fields in classes that wish to associate state with a thread (e.g., * a user ID or Transaction ID). */
大意是ThreadLocal類提供了一個線程的本地變量,每個線程持有一個這個變量的副本而且每一個線程讀取get()
到的值是不同的,能夠經過set()
方法設置這個值;
在某些狀況下使用ThreadLocal能夠避免共享資源的競爭,同時與不影響線程的隔離性。數組
經過threadLocal.set方法將對象實例保存在每一個線程本身所擁有的threadLocalMap中,
這樣每一個線程使用本身保存的ThreadLocalMap對象,不會影響線程之間的隔離。數據結構
看到這裏的第一眼我一直覺得ThreadLocal是一個map,每個線程都是一個key,對應一個value,可是是不正確的。正確的是每一個線程持有一個ThreadLocalMap的副本,這個map的鍵是ThreadLocal對象,各個線程中同一key對應的值能夠不同。less
ThreadLocal
中的字段與構造方法詳細說明參考註釋ide
public class ThreadLocal<T> { //當前ThreadLocal對象的HashCode值, //經過這個值能夠定位Entry對象在ThreadLocalMap中的位置 //由nextHashCode計算得出 private final int threadLocalHashCode = nextHashCode(); //一個自動更新的AtomicInteger值,官方解釋是會自動更新,怎麼更新的不知道, //看完AtomicInteger源碼回來填坑 private static AtomicInteger nextHashCode = new AtomicInteger(); //ThreadLocal的魔數 //0x61c88647是斐波那契散列乘數,它的優勢是經過它散列(hash)出來的結果分佈會比較均勻,能夠很大程度上避免hash衝突, private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { //原子操做:將給定的兩個值相加 return nextHashCode.getAndAdd(HASH_INCREMENT); } /** * 返回當前線程變量的初始值 * 這個方法僅在沒有調用set方法的時候第一次調用get方法調用 */ protected T initialValue() { return null; } //構造方法 public ThreadLocal() { } //建立ThreadLocalMap, //當前的ThreadLocal對象和value加入map當中 //賦值給當前線程的threadLocals字段 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } ....... }
get()、set()、remove
get()
方法
get()
方法的源碼,具體的代碼解釋請看註釋工具//返回當前線程中保存的與當前ThreadLocal相關的線程變量的值(有點繞,能夠看代碼註釋) public T get() { Thread t = Thread.currentThread(); //返回當前線程的threadLocals字段的值,類型是ThreadLocalMap //暫時能夠將ThreadLocalMap看成HashMap,下文解釋 ThreadLocalMap map = getMap(t); if (map != null) { //Entry也能夠按照HashMap的entry理解 //Entry保存了兩個值,一個值是key,一個值是value //返回當前ThreadLocalMap中當前ThreadLcoal對應的Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; //返回Entry中對應的值 return result; } } //若是當前線程的ThreadLocalMap不存在,則構造一個 return setInitialValue(); }
getMap(Thread t)
方法性能//返回當前線程的threadLocals字段的值,類型爲ThreadLocalMap ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
getEntry(ThreadLocal<?> key)
方法this//table是一個數組,具體的能夠看下文的ThreadLocalMap解釋 //返回當前ThreadLocalMap中key對應的Entry private Entry getEntry(ThreadLocal<?> key) { //根據key值計算所屬Entry所在的索引位置(同HashMap) int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; //因爲存在散列衝突,判斷當前節點是不是對應的key的節點 if (e != null && e.get() == key) //返回這個節點 return e; else return getEntryAfterMiss(key, i, e); }
setInitialValue()
方法spaprivate T setInitialValue() { //在構造方法部分有寫,返回一個初始值, //默認狀況(沒有被子類重寫)下是一個null值 T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //再次判斷當前線程中threadLocals字段是否爲空值 if (map != null) //set能夠看做HashMap中的put map.set(this, value); else //當map爲null時 //構造一個ThreadLocalMap, //並以自身爲鍵,initialValue()的結果爲值插入ThreadLocalMap //並賦值給當前線程的threadLocals字段 createMap(t, value); return value; }
createMap
方法線程void createMap(Thread t, T firstValue) { //調用THreadLocalMap的構造方法 //構造一個ThreadLocalMap, //使用給定的值構造一個存儲的實例(Entry的對象)存儲到map中 //並保存到thread的threadLocals字段中 t.threadLocals = new ThreadLocalMap(this, firstValue); }
從
get()
方法中大概能夠看出ThreadLocalMap
是一個以ThreadLocal
對象爲鍵一個Map,而且這個ThreadLocalMap
對象由Thread
類維護,並保存在threadLocals
字段中,不一樣的ThreadLocal
對象能夠以自身爲鍵訪問這個Map
中對應位置的值。當第一次調用
get()(以前沒有調用過set()或者調用了remove())
時,會調`initialValue()添加當前ThreadLocal對象對應的值爲null並返回。
set()
方法
set()
方法的源碼,具體的代碼的解釋請看註釋/** * 設定當前線程的線程變量的副本爲指定值 * 子類通常不用重寫這個方法 * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */ public void set(T value) { Thread t = Thread.currentThread(); //返回當前線程的threadLocals字段的值,類型是ThreadLocalMap,同get()方法 ThreadLocalMap map = getMap(t); //同get()同樣 判斷當前線程的threadLocals字段的是否爲null if (map != null) //不爲null,設置當前ThreadLocal(key)對應的值(value)爲指定的value map.set(this, value); else //null,建立ThreadLocalMap對象,將[t, value]加入map,並賦值給當前線程的localThreads字段 createMap(t, value); }
remove()
方法
remove()
方法的源碼,具體代碼的解釋請看註釋public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //從當前線程的threadLocals字段中移除當前ThreadLocal對象爲鍵的鍵值對 //remove方法在 ThreadLocalMap中實現 m.remove(this); }
從上面的實現代碼看,get()、set()、remove
這三個方法都是很簡單的從一個ThreadLocalMap中獲取設置或者移除值,那麼有一個核心就是ThreadLocalMap
,那麼下面就分析下ThreadLocalMap
類
我的以爲replaceStaleEntry()
、expungeStaleEntry()
、cleanSomeSlots()
這三個方法是ThreadLocal中很是重要難以理解的方法;
/** * ThreadLocalMap 是一個定製的哈希散列映射,僅僅用用來維護線程本地變量 * 對其的全部操做都在ThreadLocal類裏面。 * 使用軟引用做爲這個哈希表的key值(軟引用引用的對象在強引用解除引用後的下一次GC會被釋放) * 因爲不使用引用隊列,表裏的數據只有在表空間不足時纔會被釋放 * (由於使用的時key-value,在key被釋放·null·後這個表對應的位置不會變爲null,須要手動釋放) * 這個map和HashMap不一樣的地方是, * 在發生哈希衝突的時候HashMap會使用鏈表(jdk8以後也多是紅黑樹)存儲(拉鍊法) * 而這裏使用的是向後索引爲null的表項來存儲(開放地址法) */ static class ThreadLocalMap { /** * 這個hash Map的條目繼承了 WeakReference, 使用他的ref字段做爲key(一個ThreadLocal對象) * ThreadLocal object). 注意當鍵值爲null時表明整個鍵已經再也不被引用(ThreadLocal * 對象已經被垃圾回收)所以能夠刪除對應的條目 */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { //key值是當前TreadLocal對象的弱引用 super(k); value = v; } } //類的屬性字段,基本和HashMap做用一致 //初始容量,必須是2的冪 private static final int INITIAL_CAPACITY = 16; //哈希表 //長度必須是2的冪,必要時會調整大小 private Entry[] table; //表中存儲數據的個數 private int size = 0; //當表中數據的個數達到這個值時須要擴容 private int threshold; // Default to 0 //設置threshold ,負載係數時2/3,也就是說當前表中數據的條目 //達到表總容量的2/3就須要擴容 private void setThreshold(int len) { threshold = len * 2 / 3; } /** * Increment i modulo len. */ //這個註釋不明白,可是在代碼實現中遍歷表的的時候用來判斷是否到了表的結尾 //若是到了表節位就從表首接着這遍歷 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } /** * Decrement i modulo len. */ //這個註釋不明白,可是在代碼實現中遍歷表的的時候用來判斷是否到了表的頭部 //若是到了表節位就從表尾接着這遍歷 private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } //構造一個新map包含 (firstKey, firstValue). ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //構造表 table = new Entry[INITIAL_CAPACITY]; //肯定須要加入的數據的位置 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 構造一個新的Entry對象並放入到表的對應位置 table[i] = new Entry(firstKey, firstValue); //設置當前表中的數據個數 size = 1; // 設置須要擴容的臨界點 setThreshold(INITIAL_CAPACITY); } //這個方法在建立一個新線程調用到Thread.init()方法是會被調用 //目的是將父線程的inheritableThreadLocals傳遞給子線程 //建立的map會被存儲在Thread.inheritableThreadLocals中 //根據parentMap構造一個新的ThreadLocalMap, //這個map包含了全部parentMap的值 //只有在createInheritedMap調用 private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); //根據parentMap的長度(容量)構造table table = new Entry[len]; //依次複製parentMap中的數據 for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { //childValue在InheritableThreadLocal中實現 //也只有InheritableThreadLocal對象會調用這個方法 Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); //解決哈希衝突的辦法是向後索引 while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } }
getEntry()
獲取指定key
對應的Entry
對象
/** * 經過key獲取key-value對. 這個方法自己只處理快速路徑(直接命中) * 若是沒有命中繼續前進(getEntryAfterMiss) * 這是爲了使命中性能最大化設計 */ private Entry getEntry(ThreadLocal<?> key) { //計算key應該出如今表中的位置 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; //判斷獲取到的entry的key是不是給出的key if (e != null && e.get() == key) //是就返回entry return e; else //不然向後查找, return getEntryAfterMiss(key, i, e); } /** * getEntry的派生,當直接哈希的槽裏找不到鍵的時候使用 * * @param key the thread local object * @param i key在table中哈希的結果 * @param e the entry at table[i] * @return the entry associated with key, or null if no such */ private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; //根絕ThreadLocalMapc插入的方法,插入時經過哈希計算出來的槽位不爲null //則向後索引,找到一個空位放置須要插入的值 //因此從哈希計算的槽位到插入值的位置中間必定是不爲null的 //由於e!=null能夠做爲循環終止條件 while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) //若是命中則返回 return e; if (k == null) //e!=null && k==null,證實對應的ThreadLcoal對象已經被釋放 //那麼這個位置的entry就能夠被釋放 //釋放位置i上的空間 //釋放空間也是ThreadLocalMap與HashMap不相同的地方 expungeStaleEntry(i); else //獲取下一個查找的表的索引下標 //當i>=len時會從0號位從新開始查找 i = nextIndex(i, len); e = tab[i]; } //沒找到返回null return null; }
set()
修改或者建立指定的key
對應的Entry
對象
//添加一個key-value對 private void set(ThreadLocal<?> key, Object value) { // 不像get同樣使用快速路徑, // set建立新條目和修改現有條目同樣常見 // 這種狀況下快速路徑一般會失敗 Entry[] tab = table; int len = tab.length; //計算應該插入的槽的位置 int i = key.threadLocalHashCode & (len-1); //哈希計算的槽位到插入值的位置中間必定是不爲null的 //該位置是否位null能夠做爲循環終止條件 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //修改現有的鍵值對的值 if (k == key) { e.value = value; return; } //e!=null && k==null,證實對應的ThreadLcoal對象已經被釋放 //那麼這個位置的entry就能夠被釋放 //釋放位置i上的空間 //釋放空間也是ThreadLocalMap與HashMap不相同的地方 if (k == null) { replaceStaleEntry(key, value, i); return; } } //在能夠插入值的地方插入 tab[i] = new Entry(key, value); int sz = ++size; //清除部分k是null的槽而後判斷是否須要擴容 if (!cleanSomeSlots(i, sz) && sz >= threshold) //擴容 rehash(); }
remove()
移除指定key
對應的Entry
對象
/** * Remove the entry for key. */ private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //同set() for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { //弱引用引用置空,標記這個槽已是舊槽 e.clear(); //清理舊槽 expungeStaleEntry(i); return; } } }
set()
過程當中處理舊槽的核心方法——replaceStaleEntry()
/** * Replace a stale entry encountered during a set operation * with an entry for the specified key. The value passed in * the value parameter is stored in the entry, whether or not * an entry already exists for the specified key. * * As a side effect, this method expunges all stale entries in the * "run" containing the stale entry. (A run is a sequence of entries * between two null slots.) * * @param key the key * @param value the value to be associated with key * @param staleSlot index of the first stale entry encountered while * searching for key. */ //run:兩個空槽中間全部的非空槽 //姑且翻譯成運行區間 private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // 備份檢查當前運行中的之前的陳舊條目. // 咱們每次清理整個運行區間,避免垃圾收集器一次釋放過多的引用 // 而致使增量的哈希 // slotToExpunge刪除的槽的起始位置,由於在後面清除(expungeStaleEntry) // 的過程當中會掃描從當前位置到第一個空槽之間的位置,因此這裏只須要判斷出 // 掃描的開始位置就能夠 int slotToExpunge = staleSlot; //向前掃描找到最前面的舊槽 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 //從傳進來的舊槽的位置日後查找key值或者第一個空槽結束 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // 若是在staleSlot位置的槽後面找到了指定的key,那麼將他和staleSlot位置的槽進行交換 // 以保持哈希表的順序 // 而後將新的舊槽護着上面遇到的任何過時的槽經過expungeStaleEntry刪除 // 或者從新哈希全部運行區間的其餘條目 //找到了對應的key,將他和staleSlot位置的槽進行交換 if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // 判斷清理舊槽的開始位置 // 若是在將staleSlot以前沒有舊槽,那麼就從當前位置爲起點清理 if (slotToExpunge == staleSlot) slotToExpunge = i; //expungeStaleEntry清理從給定位置開始到第一個null的區間的空槽 //並返回第一個null槽的位置p //cleanSomeSlots從expungeStaleEntry返回的位置p開始清理log(len)次表 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); //將給定的key-value已經存放,也清理了相應運行區間 ,返回 return; } // 若是在向後查找後沒有找到對應的key值,而當前的槽是舊槽 // 同時若是在向前查找中也沒查找到舊槽 // 那麼進行槽清理的開始位置就是當前位置 //爲何不是staleSlot呢?由於在這個位置建立指定的key-value存放 if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // 若是在向後查找後沒有找到對應的key值,在staleSlot位置建立該key-value tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // 肯定掃描過程當中發現過空槽 if (slotToExpunge != staleSlot) //清理 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
replaceStaleEntry()
計算了好久的掃描清理的起點位置,總結下應該分爲四種狀況:
prev
方向上沒有舊槽,在next
方向上找到key
值以前沒有找到舊槽,那麼就交換key
和staleSlot
而後從當前位置向後清理空槽prev
方向上沒有舊槽,在next
方向上沒有找到key
沒有找到舊槽,那麼在staleSlot
位置建立指定的key-value
,prev
方向上沒有舊槽,在next
方向上沒有找到key
可是找到舊槽,那麼在staleSlot
位置建立指定的key-value
,並從找到的第一個舊槽的位置開始清理舊槽prev
方向上找到舊槽,在next
方向上沒有找到key
,那麼在staleSlot
位置建立指定的key-value
,從prev
方向最後一個找到的舊槽開始清理舊槽prev
方向上找到舊槽,在next
方向上找到key
,那麼就交換key
和staleSlot
,從prev
方向最後一個找到的舊槽開始清理舊槽求推薦個好看的畫圖工具
清理舊槽的核心方法——expungeStaleEntry()
和cleanSomeSlots()
/** * 刪除指定位置上的舊條目,並掃描從當前位置開始到第一個發現的空位之間的陳舊條目 * 還會刪除尾隨的第一個null以前的全部舊條目 */ private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 釋放槽 tab[staleSlot].value = null; tab[staleSlot] = null; size--; // 從新哈希直到遇到null Entry e; int i; //第staleSlot槽已經空出來, //從下一個槽開始掃描舊條目直到遇到空槽 //由於整個表只適用2/3的空間,因此必然會遇到空槽 //刪除掃描期間遇到的空槽 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. //尋找到從第h位開始的第一個空槽放置 while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } //返回掃描過程當中遇到的第一個空槽 return i; } /** * Heuristically scan some cells looking for stale entries. * This is invoked when either a new element is added, or * another stale one has been expunged. It performs a * logarithmic number of scans, as a balance between no * scanning (fast but retains garbage) and a number of scans * proportional to number of elements, that would find all * garbage but would cause some insertions to take O(n) time. * * @param i a position known NOT to hold a stale entry. The * scan starts at the element after i. * * @param n scan control: {@code log2(n)} cells are scanned, * unless a stale entry is found, in which case * {@code log2(table.length)-1} additional cells are scanned. * When called from insertions, this parameter is the number * of elements, but when from replaceStaleEntry, it is the * table length. (Note: all this could be changed to be either * more or less aggressive by weighting n instead of just * using straight log n. But this version is simple, fast, and * seems to work well.) * * @return true if any stale entries have been removed. */ //掃描一部分表清理其中的就條目, //掃描log(n)個 private boolean cleanSomeSlots(int i, int n) { //在此次清理過程當中是否清理了部分槽 boolean removed = false; Entry[] tab = table; int len = tab.length; do { //從i的下一個開始查找(第i個在調用這個方法前已經查找) i = nextIndex(i, len); Entry e = tab[i]; //key=e.get(),e不爲null,但key爲null if (e != null && e.get() == null) { n = len; //標記清理過某個槽 removed = true; //清理過程 i = expungeStaleEntry(i); } //只掃描log(n)個槽,一方面能夠保證避免過多的空槽的堆積 //一方面能夠保證插入或者刪除的效率 //由於刪除的時候會掃描兩個空槽以前的槽位 //每一個槽位所有掃描的話時間複雜度會高, //由於在expungeStaleEntry掃描當前位置到第一個空槽之間全部的舊槽 //因此在這裏進行每一個槽位的掃描會作不少重複的事情,效率低下 //雖然掃描從i開始的log(n)也會有不少重複掃描,可是經過優良的哈希算法 //能夠減小哈希衝突也就能夠減小重複掃描的數量 } while ( (n >>>= 1) != 0); return removed; } /** * Re-pack and/or re-size the table. First scan the entire * table removing stale entries. If this doesn't sufficiently * shrink the size of the table, double the table size. */ private void rehash() { expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 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; } /** * Expunge all stale entries in the table. */ 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); } } }
gc
,對象被回收後弱引用就變爲null,這時候就能夠進行判斷這個位置的條目是否已是舊條目,Entry
對象不被回收形成內存泄漏問題:在get()
中碰到舊槽會經過expungeStaleEntry()
方法來清理舊槽,set()
方法中調用replaceStaleEntry
-->expungeStaleEntry()
清理整個運行區間內的舊槽,cleanSomeSlots()
循環掃描log(n)個槽位進行清理,使用優秀的哈希算法減小每次調用到expungeStaleEntry()
重複清理同一區間的工做;\(\color{#FF3030}{轉載請標明出處}\)