微信公衆號:I am CR7
若有問題或建議,請在下方留言;
最近更新:2019-01-12php
繼上一篇文章《Spring Cloud Netflix Zuul源碼分析之請求處理篇》中提到的RequestContext使用的兩大神器之一:ThreadLocal,本文特此進行深刻分析,爲你們掃清知識障礙。java
在展開深刻分析以前,我們先來看一個官方示例:算法
出處來源於ThreadLocal類上的註釋,其中main方法是筆者加上的。數組
1import java.util.concurrent.atomic.AtomicInteger;
2
3public class ThreadId {
4 // Atomic integer containing the next thread ID to be assigned
5 private static final AtomicInteger nextId = new AtomicInteger(0);
6
7 // Thread local variable containing each thread's ID
8 private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
9 @Override
10 protected Integer initialValue() {
11 return nextId.getAndIncrement();
12 }
13 };
14
15 // Returns the current thread's unique ID, assigning it if necessary
16 public static int get() {
17 return threadId.get();
18 }
19
20 public static void main(String[] args) {
21 for (int i = 0; i < 5; i++) {
22 new Thread(new Runnable() {
23 @Override
24 public void run() {
25 System.out.println("threadName=" + Thread.currentThread().getName() + ",threadId=" + ThreadId.get());
26 }
27 }).start();
28 }
29 }
30}
複製代碼
運行結果以下:bash
1threadName=Thread-0,threadId=0
2threadName=Thread-1,threadId=1
3threadName=Thread-2,threadId=2
4threadName=Thread-3,threadId=3
5threadName=Thread-4,threadId=4
複製代碼
我問:看完這個例子,您知道ThreadLocal是幹什麼的了嗎?
您答:不知道,沒感受,一個hello world的例子,徹底激發不了個人興趣。
您問:那個誰,你敢不敢舉一個生產級的、工做中真實能用的例子?
我答:得,您是"爺",您說啥我就作啥。還記得《Spring Cloud Netflix Zuul源碼分析之請求處理篇》中提到的RequestContext嗎?這就是一個生產級的運用啊。Zuul核心原理是什麼?就是將請求放入過濾器鏈中通過一個個過濾器的處理,過濾器之間沒有直接的調用關係,處理的結果都是存放在RequestContext裏傳遞的,而這個RequestContext就是一個ThreadLocal類型的對象啊!!!微信
1public class RequestContext extends ConcurrentHashMap<String, Object> {
2
3 protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
4 @Override
5 protected RequestContext initialValue() {
6 try {
7 return contextClass.newInstance();
8 } catch (Throwable e) {
9 throw new RuntimeException(e);
10 }
11 }
12 };
13
14 public static RequestContext getCurrentContext() {
15 if (testContext != null) return testContext;
16
17 RequestContext context = threadLocal.get();
18 return context;
19 }
20}
複製代碼
以Zuul中前置過濾器DebugFilter爲例:app
1public class DebugFilter extends ZuulFilter {
2
3 @Override
4 public Object run() {
5 // 獲取ThreadLocal對象RequestContext
6 RequestContext ctx = RequestContext.getCurrentContext();
7 // 它是一個map,能夠放入數據,給後面的過濾器使用
8 ctx.setDebugRouting(true);
9 ctx.setDebugRequest(true);
10 return null;
11 }
12
13}
複製代碼
您問:那說了半天,它究竟是什麼,有什麼用,能不能給個概念?
我答:能!必須能!!!ide
它是啥?它是一個支持泛型的java類啊,拋開裏面的靜態內部類ThreadLocalMap不說,其實它沒幾行代碼,不信,您本身去看看。它用來幹啥?類上註釋說的很明白:源碼分析
愛提問的您,必定會有疑惑,demo裏只是調用了ThreadLocal.get()方法,它如何實現這偉大的一切呢?這就是筆者下面要講的內容,走着~~~post
話很少說,咱們來看get方法內部實現:
1public T get() {
2 Thread t = Thread.currentThread();
3 ThreadLocalMap map = getMap(t);
4 if (map != null) {
5 ThreadLocalMap.Entry e = map.getEntry(this);
6 if (e != null) {
7 @SuppressWarnings("unchecked")
8 T result = (T)e.value;
9 return result;
10 }
11 }
12 return setInitialValue();
13}
複製代碼
邏輯很簡單:
1private T setInitialValue() {
2 T value = initialValue();
3 Thread t = Thread.currentThread();
4 ThreadLocalMap map = getMap(t);
5 if (map != null)
6 map.set(this, value);
7 else
8 createMap(t, value);
9 return value;
10}
複製代碼
邏輯也很簡單:
爲了便於理解,筆者特意畫了一個時序圖,請看:
至此,您能回答ThreadLocal的實現原理了嗎?沒錯,map,一個叫作ThreadLocalMap的map,這是關鍵。每個線程都有一個私有變量,是ThreadLocalMap類型。當爲線程添加ThreadLocal對象時,就是保存到這個map中,因此線程與線程間不會互相干擾。總結起來,一句話:我有個人young,哦,不對,是我有個人map。弄清楚了這些,是否是使用的時候就自信了不少。可是,這是否是就意味着能夠大膽的去使用了呢?其實,不盡然,有一個「大坑」在等着你。
那個「大坑」指的就是由於ThreadLocal使用不當,會引起內存泄露的問題。筆者給出兩段示例代碼,來講明這個問題。
代碼出處來源於Stack Overflow:stackoverflow.com/questions/1…
1public class MemoryLeak {
2
3 public static void main(String[] args) {
4 new Thread(new Runnable() {
5 @Override
6 public void run() {
7 for (int i = 0; i < 1000; i++) {
8 TestClass t = new TestClass(i);
9 t.printId();
10 t = null;
11 }
12 }
13 }).start();
14 }
15
16 static class TestClass{
17 private int id;
18 private int[] arr;
19 private ThreadLocal<TestClass> threadLocal;
20 TestClass(int id){
21 this.id = id;
22 arr = new int[1000000];
23 threadLocal = new ThreadLocal<>();
24 threadLocal.set(this);
25 }
26
27 public void printId(){
28 System.out.println(threadLocal.get().id);
29 }
30 }
31}
複製代碼
運行結果:
10
21
32
43
5...省略...
6440
7441
8442
9443
10444
11Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
12 at com.gentlemanqc.MemoryLeak$TestClass.<init>(MemoryLeak.java:33)
13 at com.gentlemanqc.MemoryLeak$1.run(MemoryLeak.java:16)
14 at java.lang.Thread.run(Thread.java:745)
複製代碼
對上述代碼稍做修改,請看:
1public class MemoryLeak {
2
3 public static void main(String[] args) {
4 new Thread(new Runnable() {
5 @Override
6 public void run() {
7 for (int i = 0; i < 1000; i++) {
8 TestClass t = new TestClass(i);
9 t.printId();
10 t.threadLocal.remove();
11 }
12 }
13 }).start();
14 }
15
16 static class TestClass{
17 private int id;
18 private int[] arr;
19 private ThreadLocal<TestClass> threadLocal;
20 TestClass(int id){
21 this.id = id;
22 arr = new int[1000000];
23 threadLocal = new ThreadLocal<>();
24 threadLocal.set(this);
25 }
26
27 public void printId(){
28 System.out.println(threadLocal.get().id);
29 }
30 }
31}
複製代碼
運行結果:
10
21
32
43
5...省略...
6996
7997
8998
9999
複製代碼
一個內存泄漏,一個正常完成,對比代碼只有一處不一樣:t = null改成了t.threadLocal.remove(); 哇,神奇的remove!!!筆者先留個懸念,暫且不去分析緣由。咱們先來看看上述示例中涉及到的兩個方法:set()和remove()。
1public void set(T value) {
2 Thread t = Thread.currentThread();
3 ThreadLocalMap map = getMap(t);
4 if (map != null)
5 map.set(this, value);
6 else
7 createMap(t, value);
8}
複製代碼
邏輯很簡單:
1public void remove() {
2 ThreadLocalMap m = getMap(Thread.currentThread());
3 if (m != null)
4 m.remove(this);
5}
複製代碼
就一句話,獲取當前線程內部的ThreadLocalMap,存在則從map中刪除這個ThreadLocal對象。
講到這裏,ThreadLocal最經常使用的四種方法都已經說完了,細心的您是否是已經發現,每個方法都離不開一個類,那就是ThreadLocalMap。因此,要更好的理解ThreadLocal,就有必要深刻的去學習這個map。
仍是老規矩,先來看看類上的註釋,翻譯過來就是這麼幾點:
咱們來看下類的聲明信息:
1static class ThreadLocalMap {
2
3 // hash map中的entry繼承自弱引用WeakReference,指向threadLocal對象
4 // 對於key爲null的entry,說明再也不須要訪問,會從table表中清理掉
5 // 這種entry被成爲「stale entries」
6 static class Entry extends WeakReference<ThreadLocal<?>> {
7 /** The value associated with this ThreadLocal. */
8 Object value;
9
10 Entry(ThreadLocal<?> k, Object v) {
11 super(k);
12 value = v;
13 }
14 }
15
16 /**
17 * The initial capacity -- MUST be a power of two.
18 */
19 private static final int INITIAL_CAPACITY = 16;
20
21 /**
22 * The table, resized as necessary.
23 * table.length MUST always be a power of two.
24 */
25 private Entry[] table;
26
27 /**
28 * The number of entries in the table.
29 */
30 private int size = 0;
31
32 /**
33 * The next size value at which to resize.
34 */
35 private int threshold; // Default to 0
36
37 /**
38 * Set the resize threshold to maintain at worst a 2/3 load factor.
39 */
40 private void setThreshold(int len) {
41 threshold = len * 2 / 3;
42 }
43
44 /**
45 * Construct a new map initially containing (firstKey, firstValue).
46 * ThreadLocalMaps are constructed lazily, so we only create
47 * one when we have at least one entry to put in it.
48 */
49 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
50 table = new Entry[INITIAL_CAPACITY];
51 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
52 table[i] = new Entry(firstKey, firstValue);
53 size = 1;
54 setThreshold(INITIAL_CAPACITY);
55 }
56}
複製代碼
當建立一個ThreadLocalMap時,實際上內部是構建了一個Entry類型的數組,初始化大小爲16,閾值threshold爲數組長度的2/3,Entry類型爲WeakReference,有一個弱引用指向ThreadLocal對象。
Java垃圾回收時,看一個對象需不須要回收,就是看這個對象是否可達。什麼是可達,就是能不能經過引用去訪問到這個對象。(固然,垃圾回收的策略遠比這個複雜,這裏爲了便於理解,簡單給你們說一下)。
jdk1.2之後,引用就被分爲四種類型:強引用、弱引用、軟引用和虛引用。強引用就是咱們經常使用的Object obj = new Object(),obj就是一個強引用,指向了對象內存空間。當內存空間不足時,Java垃圾回收程序發現對象有一個強引用,寧願拋出OutofMemory錯誤,也不會去回收一個強引用的內存空間。而弱引用,即WeakReference,意思就是當一個對象只有弱引用指向它時,垃圾回收器無論當前內存是否足夠,都會進行回收。反過來講,這個對象是否要被垃圾回收掉,取決因而否有強引用指向。ThreadLocalMap這麼作,是不想由於本身存儲了ThreadLocal對象,而影響到它的垃圾回收,而是把這個主動權徹底交給了調用方,一旦調用方不想使用,設置ThreadLocal對象爲null,內存就能夠被回收掉。
至此,該作的鋪墊都已經完成了,此時,咱們能夠來看看上面那個內存泄漏的例子。示例中執行一次for循環裏的代碼後,對應的內存狀態:
- t爲建立TestClass對象返回的引用,臨時變量,在一次for循環後就執行出棧了
- thread爲建立Thread對象返回的引用,run方法在執行過程當中,暫時不會執行出棧
調用t=null後,雖然沒法再經過t訪問內存地址MemoryLeak
1不能識別此Latex公式:
2TestClass@538,可是當前線程依舊存活,能夠經過thread指向的內存地址,訪問到Thread對象,從而訪問到ThreadLocalMap對象,訪問到value指向的內存空間,訪問到arr指向的內存空間,從而致使Java垃圾回收並不會回收int[1000000]@541這一片空間。那麼隨着循環屢次以後,不被回收的堆空間愈來愈大,最後拋出java.lang.OutOfMemoryError: Java heap space。
3
4您問:那爲何調用t.threadLocal.remove()就能夠呢?
5
6我答:這就得看remove方法裏究竟作了什麼了,請看:
7
8是否是恍然大悟?來看下調用remove方法以後的內存狀態:
9
10由於remove方法將referent和value都被設置爲null,因此ThreadLocal@540和Memory
複製代碼
TestClass@538對應的內存地址都變成不可達,Java垃圾回收天然就會回收這片內存,從而不會出現內存泄漏的錯誤。
呼應文章開頭提到的《Spring Cloud Netflix Zuul源碼分析之請求處理篇》,其中就有一個很是重要的類:ZuulServlet,它就是典型的ThreadLocal在實際場景中的運用案例。請看:
1public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
2 try {
3 init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
4 RequestContext context = RequestContext.getCurrentContext();
5 context.setZuulEngineRan();
6
7 try {
8 preRoute();
9 } catch (ZuulException e) {
10 error(e);
11 postRoute();
12 return;
13 }
14 try {
15 route();
16 } catch (ZuulException e) {
17 error(e);
18 postRoute();
19 return;
20 }
21 try {
22 postRoute();
23 } catch (ZuulException e) {
24 error(e);
25 return;
26 }
27
28 } catch (Throwable e) {
29 error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
30 } finally {
31 RequestContext.getCurrentContext().unset();
32 }
33}
複製代碼
您有沒有發現,一次HTTP請求經由前置過濾器、路由過濾器、後置過濾器處理完成以後,都會調用一個方法,沒錯,就是在finally裏,RequestContext.getCurrentContext().unset()。走進RequestContext一看:
1public void unset() {
2 threadLocal.remove();
3}
複製代碼
看到沒有,神器的remove又出現了。講到這裏,您是否get到ThreadLocal正確的使用"姿式"呢?
筆者以前寫過關於TreeMap和HashMap的文章,凡是Map的實現,都有本身下降哈希衝突和解決哈希衝突的方法。在這裏,ThreadLocalMap是如何處理的呢?請往下看。
回顧ThreadLocalMap添加元素的源碼:
1ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
2 table = new Entry[INITIAL_CAPACITY];
3 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
4 table[i] = new Entry(firstKey, firstValue);
5 size = 1;
6 setThreshold(INITIAL_CAPACITY);
7}
複製代碼
1private void set(ThreadLocal<?> key, Object value) {
2
3 Entry[] tab = table;
4 int len = tab.length;
5 int i = key.threadLocalHashCode & (len-1);
6
7 for (Entry e = tab[i];
8 e != null;
9 e = tab[i = nextIndex(i, len)]) {
10 ThreadLocal<?> k = e.get();
11
12 if (k == key) {
13 e.value = value;
14 return;
15 }
16
17 if (k == null) {
18 replaceStaleEntry(key, value, i);
19 return;
20 }
21 }
22
23 tab[i] = new Entry(key, value);
24 int sz = ++size;
25 if (!cleanSomeSlots(i, sz) && sz >= threshold)
26 rehash();
27}
複製代碼
其中i就是ThreadLocal在ThreadLocalMap中存放的索引,計算方式爲:key.threadLocalHashCode & (len-1)。咱們先來看threadLocalHashCode是什麼?
1private final int threadLocalHashCode = nextHashCode();
複製代碼
也就是說,每個ThreadLocal都會根據nextHashCode生成一個int值,做爲哈希值,而後根據這個哈希值&(數組長度-1),從而獲取到哈希值的低N位(以len爲16,16-1保證低四位都是1,從而獲取哈希值自己的低四位值),從而獲取到在數組中的索引位置。那它是如何下降哈希衝突的呢?玄機就在於這個nextHashCode方法。
1private static AtomicInteger nextHashCode = new AtomicInteger();
2
3private static final int HASH_INCREMENT = 0x61c88647;
4
5private static int nextHashCode() {
6 return nextHashCode.getAndAdd(HASH_INCREMENT);
7}
複製代碼
0x61c88647是什麼?轉化爲十進制是1640531527。2654435769轉換成int類型就是-1640531527。2654435769等於(根號5-1)/2乘以2的32次方。(根號5-1)/2是什麼?是黃金分割數,近似爲0.618。也就是說0x61c88647理解爲一個黃金分割數乘以2的32次方。有什麼好處?它能夠神奇的保證nextHashCode生成的哈希值,均勻的分佈在2的冪次方上,且小於2的32次方。來看例子:
1public class ThreadLocalHashCodeTest {
2
3 private static AtomicInteger nextHashCode =
4 new AtomicInteger();
5
6 private static final int HASH_INCREMENT = 0x61c88647;
7
8 private static int nextHashCode() {
9 return nextHashCode.getAndAdd(HASH_INCREMENT);
10 }
11
12 public static void main(String[] args){
13 for (int i = 0; i < 16; i++) {
14 System.out.print(nextHashCode() & 15);
15 System.out.print(" ");
16 }
17 System.out.println();
18 for (int i = 0; i < 32; i++) {
19 System.out.print(nextHashCode() & 31);
20 System.out.print(" ");
21 }
22 System.out.println();
23 for (int i = 0; i < 64; i++) {
24 System.out.print(nextHashCode() & 63);
25 System.out.print(" ");
26 }
27 }
28}
複製代碼
輸出結果:
10 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9
216 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9
316 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9
複製代碼
看見沒有,元素索引值完美的散列在數組當中,並無出現衝突。
ThreadLocalMap採用黃金分割數的方式,大大下降了哈希衝突的狀況,可是這種狀況仍是存在的,那若是出現,它是怎麼解決的呢?請看:
1private void set(ThreadLocal<?> key, Object value) {
2
3 Entry[] tab = table;
4 int len = tab.length;
5 int i = key.threadLocalHashCode & (len-1);
6
7 // 出現哈希衝突
8 for (Entry e = tab[i];
9 e != null;
10 e = tab[i = nextIndex(i, len)]) {
11 ThreadLocal<?> k = e.get();
12
13 // 若是是同一個對象,則覆蓋value值
14 if (k == key) {
15 e.value = value;
16 return;
17 }
18
19 // 若是key爲null,則替換它的位置
20 if (k == null) {
21 replaceStaleEntry(key, value, i);
22 return;
23 }
24
25 // 不然日後一個位置找,直到找到空的位置
26 }
27
28 tab[i] = new Entry(key, value);
29 int sz = ++size;
30 if (!cleanSomeSlots(i, sz) && sz >= threshold)
31 rehash();
32}
複製代碼
當出現哈希衝突時,它的作法看是不是同一個對象或者是是否能夠替換,不然日後移動一位,繼續判斷。
1private static int nextIndex(int i, int len) {
2 return ((i + 1 < len) ? i + 1 : 0);
3}
複製代碼
經過set方法裏的代碼,咱們知道ThreadLocalMap擴容有兩個前提:
元素個數大於閾值進行擴容,這個很好理解,那麼還有一個前提是什麼意思呢?咱們來看cleanSomeSlots()作了什麼:
1private boolean cleanSomeSlots(int i, int n) {
2 boolean removed = false;
3 Entry[] tab = table;
4 int len = tab.length;
5 do {
6 i = nextIndex(i, len);
7 Entry e = tab[i];
8 if (e != null && e.get() == null) {
9 n = len;
10 removed = true;
11 i = expungeStaleEntry(i);
12 }
13 } while ( (n >>>= 1) != 0);
14 return removed;
15}
複製代碼
方法上註釋寫的很明白,從當前插入元素位置,日後掃描數組中的元素,判斷是不是「stale entry」。在前面將ThreadLocalMap類聲明信息的時候講過,「stale entry」表示的是那些key爲null的entry。cleanSomeSlots方法就是找到他們,調用expungeStaleEntry方法進行清理。若是找到,則返回true,不然返回false。
您問:爲何擴容要看它的返回值呢?
我答:由於一旦找到,就調用expungeStaleEntry方法進行清理。
1private int expungeStaleEntry(int staleSlot) {
2 Entry[] tab = table;
3 int len = tab.length;
4
5 // expunge entry at staleSlot
6 tab[staleSlot].value = null;
7 tab[staleSlot] = null;
8 size--;
9
10 // 省略
11}
複製代碼
看到沒有,size會減一,那麼添加元素致使size加1,cleanSomeSlots一旦找到,則會清理一個或者多個元素,size減去的最少爲1,因此返回true,天然就沒有必要再判斷size是否大於等於閾值了。
好了,前提條件一旦知足,則調用rehash方法,此時還未擴容:
1private void rehash() {
2 // 先清理stale entry,會致使size變化
3 expungeStaleEntries();
4
5 // 若是size大於等於3/4閾值,則擴容
6 if (size >= threshold - threshold / 4)
7 resize();
8}
複製代碼
哈哈,這裏纔是真正的擴容,要進行擴容:
既然搞清楚了條件,那麼知足後,又是如何擴容的呢?
1private void resize() {
2 Entry[] oldTab = table;
3 int oldLen = oldTab.length;
4 int newLen = oldLen * 2;
5 // 新建一個數組,按照2倍長度擴容
6 Entry[] newTab = new Entry[newLen];
7 int count = 0;
8
9 for (int j = 0; j < oldLen; ++j) {
10 Entry e = oldTab[j];
11 if (e != null) {
12 ThreadLocal<?> k = e.get();
13 if (k == null) {
14 e.value = null; // Help the GC
15 } else {
16 // key不爲null,從新計算索引位置
17 int h = k.threadLocalHashCode & (newLen - 1);
18 while (newTab[h] != null)
19 h = nextIndex(h, newLen);
20 // 插入新的數組中索引位置
21 newTab[h] = e;
22 count++;
23 }
24 }
25 }
26
27 // 閾值爲長度的2/3
28 setThreshold(newLen);
29 size = count;
30 table = newTab;
31}
複製代碼
兩倍長度擴容,從新計算索引,擴容的同時也順便清理了key爲null的元素,即stale entry,再也不存入擴容後的數組中。
不知您有沒有注意到,ThreadLocalMap中出現哈希衝突時,它是線性探測,直到找到空的位置。這種效率是很是低的,那爲何Java大神們寫代碼時還要這麼作呢?筆者認爲取決於它採用的哈希算法,正由於nextHashCode(),保證了衝突出現的可能性很低。並且ThreadLocalMap在處理過程當中很是注意清理"stale entry",及時釋放出空餘位置,從而下降了線性探測帶來的低效。
本文講了這麼多,主要是爲了讓你們明白ThreadLocal應該如何正確的使用,以及使用它背後的原理。後面番外篇,純屬興趣部分,您能夠對比以前筆者《HashMap之元素插入》裏面的內容,發散思考。筆者深知水平有限,若有任何意見建議,還請您留言指出,感激涕零!!!最後,感謝你們一如既往的支持,祝近安,祁琛,2019年1月12日。