ThreadLocal原理及用法詳解

背景

一直以來對ThreadLocal用法模棱兩可,不知道怎麼用今天好好研究了下給你們分享下。javascript

  • 一、講解ThreadLocal以前先回顧下什麼是取模、x^y、弱引用。

1. 取模運算其實是計算兩數相除之後的餘數.

假設a除以b的商是c,d是相對應的餘數,那麼幾乎全部的計算機系統都知足a = c* b + d。因此d=a-b*c。
 17 % 10 的計算結果以下:d = (17) - (17 / 10) x 10 = (17) - (1 x 10) = 7
 -17 % 10 的計算結果以下:d = (-17) - (-17 / 10) x 10 = (-17) - (-1 x 10) = -7
-17 % -10 的計算結果以下:d = 17 - (17 / -10) x (-10) = (17) - (-1 x -10) = 7
-17 % -10 的計算結果以下:d= (-17) - (-17 / -10) x (-10) = (-17) - (1 x -10) = -7
能夠看出:運算結果的符號始終和被模數的符號一致。java

2. x^y 按位取異或。

如:x是二進制數0101 y是二進制數1011 則結果爲x^y=1110,0^1=1,0^0=0,1^1=0,1^0=1!只要有一個爲1就取值爲1。程序員

3. 弱引用

弱引用也是用來描述非必需對象的,當JVM進行垃圾回收時,不管內存是否充足,都會回收被弱引用關聯的對象。在java中,用java.lang.ref.WeakReference類來表示。這裏所說的被弱引用關聯的對象是指只有弱引用與之關聯,若是存在強引用同時與之關聯,則進行垃圾回收時也不會回收該對象。web

  • 二、咱們要問兩個問題

1.什麼是ThreadLocal
ThreadLocal從字面意識能夠理解爲線程本地變量。也就是說若是定義了ThreadLocal,每一個線程往這個ThreadLocal中讀寫是線程隔離,互相之間不會影響的。它提供了一種將可變數據經過每一個線程有本身的獨立副本從而實現線程封閉的機制。
2.實現的思路是什麼
Tread類有一個類型爲ThreadLocal.ThreadLocalMap的實例變量threadLocals,咱們使用線程的時候有一個本身的ThreadLocalMap。ThreadLocalMap有本身的獨立實現,能夠簡單認爲ThreadLocal視爲key,value爲代碼中放入的值(實際上key並非ThreadLocal自己,經過源碼能夠知道它是一個弱引用)。調用ThreadLocal的set方法時候,都會存到ThreadLocalMap裏面。調用ThreadLocal的get方法時候,在本身map裏面找key,從而實現線程隔離。算法

  • 三、ThreadLocal最主要的實如今於ThreadLocalMap這個內部類裏面,咱們重點關注ThreadLocalMap這個類的用法,看看兩位大師Josh Bloch and Doug Lea是什麼設計出如此好的類。

  • 四、ThreadLocalMap

 
image.png

 

上面是ThreadLocalMap全部的API
ThreadLocalMap提供了一種爲ThreadLocal定製的高效實現,而且自帶一種基於弱引用的垃圾清理機制。api

  1. 存儲結構方面
    存儲結構能夠理解爲一個map,可是不要和java.util.Map弄混。
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } 

從上面代碼能夠看出Entry即是ThreadLocalMap裏定義的節點,它繼承了WeakReference類,定義了一個類型爲Object的value,用於存放塞到ThreadLocal裏的值,key能夠視爲爲ThreadLocal。數組

  1. 爲何要用弱引用,由於若是這裏使用普通的key-value形式來定義存儲結構,實質上就會形成節點的生命週期與線程強綁定,只要線程沒有銷燬,那麼節點在GC分析中一直處於可達狀態,沒辦法被回收,而程序自己也沒法判斷是否能夠清理節點。弱引用是Java四種引用的的第三種(其它三種強引用、軟引用、虛引用),比軟引用更加弱一些,若是一個對象沒有強引用鏈可達,那麼通常活不過下一次GC。當某個ThreadLocal已經沒有強引用可達,則隨着它被垃圾回收,在ThreadLocalMap裏對應的Entry的鍵值會失效,這爲ThreadLocalMap自己的垃圾清理提供了便利。
  2. Entry裏面的成員變量和方法
/** * 初始容量,必須爲2的冪. */ private static final int INITIAL_CAPACITY = 16; /** * 根據須要調整大小。 * 長度必須老是2的冪。 */ private Entry[] table; /** * 表中條目的數量。 */ private int size = 0; /** * 要調整大小的下一個大小值。默認爲0 */ private int threshold; // Default to 0 /** * 將調整大小閾值設置維持最壞2/3的負載因子。 */ private void setThreshold(int len) { threshold = len * 2 / 3; } /** *上一個索引 */ private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } /** * 下一個索引 */ private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } 

因爲ThreadLocalMap使用線性探測法來解決散列衝突,因此實際上Entry[]數組在程序邏輯上是做爲一個環形存在的。tomcat

  1. 構造函數
/** * Construct a new map initially containing (firstKey, firstValue). * ThreadLocalMaps are constructed lazily, so we only create * one when we have at least one entry to put in it. * 構造一個最初包含(firstKey, firstValue)的新映射threadlocalmap是延遲構造的,因 * 此當咱們至少有一個元素能夠放進去的時候纔去建立。 */ ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } 

重點說下這個hash函數int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
ThreadLocal類中有一個被final修飾的類型爲int的threadLocalHashCode,它在該ThreadLocal被構造的時候就會生成,至關於一個ThreadLocal的ID,而它的值來源於安全

private final int threadLocalHashCode = nextHashCode(); /** * The next hash code to be given out. Updated atomically. Starts at * zero. */ private static AtomicInteger nextHashCode = new AtomicInteger(); /** * 連續生成的哈希碼之間的區別——循環隱式順序線程本地id以近乎最優的方式展開 * 用於兩倍大小表的乘法哈希值。 */ private static final int HASH_INCREMENT = 0x61c88647; /** * 返回下一個hashcode */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } 

經過理論和實踐算出通,當咱們用0x61c88647做爲魔數累加爲每一個ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模,獲得的結果分佈很均勻。less

ThreadLocalMap使用的是線性探測法,均勻分佈的好處在於很快就能探測到下一個臨近的可用slot,從而保證效率。這就回答了上文拋出的爲何大小要爲2的冪的問題。爲了優化效率。對於& (INITIAL_CAPACITY - 1),相信有過算法閱讀源碼較多的程序員,一看就明白,對於2的冪做爲模數取模,能夠用&(2n-1)來替代%2n,位運算比取模效率高不少。至於爲何,由於對2^n取模,只要不是低n位對結果的貢獻顯然都是0,會影響結果的只能是低n位。
  1. TreadLocal中的get方法
    ThreadLocal中的get方法會調用這個ThreadLocalMap中的getEntry方法,
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 return getEntryAfterMiss(key, i, e); } /** * Version of getEntry method for use when key is not found in * its direct hash slot. * * @param key the thread local object * @param i the table index for key's hash code * @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; 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; } 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--; } 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. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; } 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--; } 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. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; } 

這上面的註釋很清楚,這裏都不在多講了

簡單說下當調用get方法時候遇到的狀況

根據入參threadLocal的threadLocalHashCode對錶容量取模獲得index若是index對應的slot就是要讀的threadLocal,則直接返回結果
調用getEntryAfterMiss線性探測,過程當中每碰到無效slot,調用expungeStaleEntry進行段清理;若是找到了key,則返回結果entry
沒有找到key,返回null

  1. TreadLocal中的set方法
/** * Set the value associated with key. * * @param key the thread local object * @param value the value to be set */ 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(); } /** * 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. */ 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). 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 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. if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } /** * 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. */ 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); return removed; } private void rehash() { expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 4) resize(); } /** * Double the capacity of the table. */ 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); } } 
簡單總結下上面的用法

a、探測過程當中slot都不無效,而且順利找到key所在的slot,直接替換便可
b、探測過程當中發現有無效slot,調用replaceStaleEntry,效果是最終必定會把key和value放在這個slot,而且會盡量清理無效slot
在replaceStaleEntry過程當中,若是找到了key,則作一個swap把它放到那個無效slot中,value置爲新值
在replaceStaleEntry過程當中,沒有找到key,直接在無效slot原地放entry
c、探測沒有發現key,則在連續段末尾的後一個空位置放上entry,這也是線性探測法的一部分。放完後,作一次啓發式清理,若是沒清理出去key,而且當前table大小已經超過閾值了,則作一次rehash,rehash函數會調用一次全量清理slot方法也expungeStaleEntries,若是完了以後table大小超過了threshold - threshold / 4,則進行擴容2倍
六、ThreadLocal中的remove方法調用TreadLocalMap中的remove

/** * Remove the entry for key. */ 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; } } } 

這個方法比較簡單,找到key就清除

  1. ThreadLocal會不會遇到內存泄漏問題
    有關於內存泄露是由於在有線程複用如線程池的場景中,一個線程的生命週期很長,大對象長期不被回收影響系統運行效率與安全。若是線程不會複用,用完即銷燬了也不會有ThreadLocal引起內存泄露的問題。
    仔細讀過ThreadLocalMap的源碼,能夠推斷,若是在使用的ThreadLocal的過程當中,習慣加上remove,這樣是不會引發內存泄漏。
    若是沒有進行remove呢?若是對應線程以後調用ThreadLocal的get和set方法都有很高的機率會順便清理掉無效對象,斷開value強引用,從而大對象被收集器回收。
    咱們應該考慮到什麼時候調用ThreadLocal的remove方法。一個比較熟悉的場景就是對於一個請求一個線程的server如tomcat,在代碼中對web api做一個切面,存放一些如用戶名等用戶信息,在鏈接點方法結束後,再顯式調用remove。

以上都是理論,咱們作一個小實驗

/** * @author shuliangzhao * @Title: ThreadLocalDemo * @ProjectName design-parent * @Description: TODO * @date 2019/6/1 0:00 */ public class ThreadLocalDemo { private static ThreadLocal<ThreadLocalDemo> t = new ThreadLocal<>(); private ThreadLocalDemo() {} public static ThreadLocalDemo getInstance() { ThreadLocalDemo threadLocalDemo = ThreadLocalDemo.t.get(); if (null == threadLocalDemo) { threadLocalDemo = new ThreadLocalDemo(); t.set(threadLocalDemo); } return threadLocalDemo; } private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public static void main(String[] args) { long i = (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1)); System.out.println(i); int i1 = 55&(2^2-1); System.out.println(55%2^2); System.out.println(i1); } } 
/** * @author shuliangzhao * @Title: ThreadLocalTest * @ProjectName design-parent * @Description: TODO * @date 2019/6/1 0:03 */ public class ThreadLocalTest { public static void main(String[] args) { for(int i=0; i<2;i++){ new Thread(new Runnable() { @Override public void run() { Double d = Math.random()*10; ThreadLocalDemo.getInstance().setName("name "+d); new A().get(); new B().get(); } }).start(); } } static class A{ public void get(){ System.out.println(ThreadLocalDemo.getInstance().getName()); } } static class B{ public void get(){ System.out.println(ThreadLocalDemo.getInstance().getName()); } } } 

運行結果

 

 
image.png

到這裏咱們就把ThreadLocal講完了,能夠多看看源碼,看看大牛們是怎麼設計出如此優美的代碼。

相關文章
相關標籤/搜索