Java并发编程之ThreadLocal源码分析
1 一句話概括ThreadLocal
??什么是ThreadLocal?顧名思義:線程本地變量,它為每個使用該對象的線程創建了一個獨立的變量副本。
2 ThreadLocal使用場景
??用一句話總結ThreadLocal真的實在是太蒼白無力了!我們通過一個簡單的例子入手。比如現在有A和B兩臺服務器需要通過http請求傳遞數據,但又希望數據安全性有一定保障,因此發送方A決定用AES算法對傳輸數據加密后再發送給B。接受方B收到數據后,通過密鑰解密數據并進行后續的業務處理。
??對數據進行AES解密,接收方B可選擇JAVA提供的Cipher類來實現。我們在調用Cipher類進行解密時時,需要獲取Cipher對象的實例,如下所示:
??接著我們調用該實例就可以進行數據解密工作。但很不幸的是,Cipher類存在線程安全問題,它無法工作于多線程場景下。簡單來說就是單個Cipher實例無法同時解密多條數據。
??那怎么辦呢?
有沒有什么辦法能讓每個線程擁有相同的instance實例,且彼此互不干擾呢?這個時候我們可以借助ThreadLocal類來實現特定功能,ThreadLocal能夠為每個使用該對象的線程創建獨立的變量的副本。這樣就滿足了我們的需求。
??后續篇幅我們會深入到ThreadLocal的源碼層來探討它的實現機制,在看ThreadLocal類的幾個基本方法前,讓我們先看一下ThreadLocal中靜態類ThreadLocalMap的實現,它對于我們理解ThreadLocal有著舉足輕重的作用!
3 ThreadLocalMap源碼分析
??ThreadLocalMap是ThreadLocal類中的一個靜態類,它擁有一個Entry數組類型的成員變量,名為table。這個Entry是啥?我們來看看。
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成員變量。當我們創建Entry對象后,調用Entry.get()方法,獲取到的實際上是ThreadLocal對象的弱引用。而這個設計則保證了Entry對象中保存的ThreadLocal弱引用是易被回收的。網上有很多關于ThreadLocal對象是否會引發內存泄漏的文章,這里的內存泄漏通常指的不是entry的key,也就是ThreadLocal的弱引用。而是這里的value對象。實際上ThreadLocalMap自身提供了一套回收無用Entry節點的機制。在后面我們會聊到它的實現。關于ThreadLocal是否會引發內存泄漏,這里暫時不做探討。
3.1 ThreadLocalMap.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(); }??ThreadLocalMap類持有一個Entry數組,名為table。當我們調用ThreadLocalMap的set()方法時,其實就是更新table中某個Entry,或往table中插入一個Entry。set()方法其實是根據threadLocal對象的threadLocalHashCode來計算當前Entry節點應落入什么位置。當然存在多個entry落入位置發生沖突的情況,在ThreadLocal中使用了線性探測法來解決沖突。知道了這一點,那set方法的實現思路就很清晰了。就是找一個位置讓我放節點嘛!如果已經有現成的了,更新一下value就行。要是找不到,那我就按線性探測法來找落入位置就好了嘛。值得注意的是replaceStaleEntry()方法!當entry節點不為空,而key為null時會調用這個方法。這個方法看名字好像是用來替換過時的Entry節點的?我們來看一下它到底是干嘛的?
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); }??這個方法好像還挺長的,上面我們已經猜測過它的用途了,現在我們就來揭開它的廬山真面目。
??首先關注一個這個方法的參數:
??第一個參數就是我們的key,估摸著還是用來計算hash值找位置的;第二個就是要放的value了;第三個staleSlot呢?我們上面好像是找到了一個key為null的entry節點吧?沒錯,這個staleSlot就是這個節點在tab中的位置了。然后從這個staleSlot節點開始往前找,如果發現某個entry不為空,但key等于null,用slotToExpunge記錄下它的位置,直到往前找到一個entry為null的節點停止。這個slotToExpunge是用來干嘛的呢?后面會提到。
??我們接著看。往前找完之后,我們又從staleSlot的下一個節點開始往后找,如果發現了某個節點的鍵值等于我們的key。我們是不是應該用我們的value替換掉這個位置原先的值呢?好像是應該替換。但是別忘記了前面還有個key為null的entry節點呢!由于之前key為null的節點和當前節點計算出來hash值其實是一樣的。這里我們將e節點的值更新為最新的value后,互換tab[i]和entry的位置。這一步的目的是什么呢?我猜大概是這樣的,因為ThreadLocalMap是根據線性探測法來解決沖突的,因此可能會出現key的哈希值相同但散落位置不連續的情況。為了在一定程度上提高查找哈希值相同entry節點的效率,交換一下位置會是更好的選擇。同時接下來會執行cleanSomeSlots()方法。我們上面的for循環會一直往后找,直到發現一個null節點為止。如果找到了null節點,那就說明按照線性探測法找不到這個節點了啊!那咋辦呢?staleSlot節點不是空著呢么。直接塞進去不就完事了。。
??最后一句又調用了cleanSomeSlots()方法。下面就輪到它了。。
??在看cleanSomeSlots()方法前,還得看expungeStaleEntry()。有時候不得不說,好的java命名規范,真的是很重要啊!看到這個方法的名字就知道它大概是用來清理過期節點的。回想一下,有什么節點是需要我們的清理的嗎?好像有。。前面是不是有找到過key為null的entry節點啊?這個key為null的節點好像沒啥用啊!
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);??還記得這個slotToExpunge嗎?不記得了的話往前翻一翻。這個slotToExpunge位置指向了一個key為null的entry節點。既然知道這個節點是沒用的,那它就應該被回收。這里就很直接粗暴了,直接把它直接null以待后面垃圾回收器清理。清理完之后,又是一個for循環。如果key為null,將該entry置為null。如果不為null,重新計算一下hash值,如果位置與當前位置不同,需要重新找一個位置放該節點。當然也是利用線性探測法了,找到連續位置后面第一個為null的節點放置。
??最后返回的節點為從slotToExpunge往后的第一個值為null的entry節點。
??再看看cleanSomeSlots方法主體。
??這個方法顧名思義是用來清理某些節點的。清理啥節點呢?還是那些不為null但是key為null的節點。參數n決定了for循環要執行的次數。>>>在java中是無符號位移的意思,也就是說如果每次循環tab[i]均不需要清理,最多會執行logn次。如果有需要清理的節點,就會調用expungeStaleEntry()方法去回收這個節點。
??上面說了這么一大堆,終于把ThreadLocalMap的set()方法說完了。下面接著來看getEntry()方法。
3.2 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); }??getEntry()方法比較簡單。先根據key值計算出對應在table中的位置,如果table[i]的key值和我們的參數key相同,直接返回table[i];反之,調用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); else i = nextIndex(i, len); e = tab[i]; } return null; }??進入到這個方法的情況可能如下三種:
如果entry節點為null,直接返回null。
執行expungeStaleEntry()方法回收該節點,回收完之后,tab[i]節點也就變成了null,直接返回null。
從i節點開始往后找,如果有key值相同的節點,也就是我們找到了我們需要的節點,返回entry即可。如果找不到,從i節點往后找,遇到key為null的回收一下該節點后返回null,遇到entry為null的直接返回null;
??getEntry()方法會被ThreadLocal的get()方法調用,我們會在稍后的ThreadLocal源碼的講解中再談。
??介紹ThreadLocalMap用了不少的篇幅啊!下面就來看看我們ThreadLocal啦!關于ThreadLocal的方法網上已經有太多太多的文章介紹了。不過這里我們還是簡單的結合我們上面所說的ThreadLocalMap來總結一下!
4 ThreadLocal源碼分析
4.1 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); }??當我們調用ThreadLocal對象的set方法時,程序會獲取當前線程,并將其作為作為參數傳遞給getMap()方法。
Thread.java ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocal.java ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocalMap getMap(Thread t) { return t.threadLocals; }??查看Thread類源碼可以發現,Thread類中包含了一個ThreadLocalMap對象,這個家伙我們上面已經花了很大的篇幅來說了,簡單的說它的key為ThreadLocal的弱引用,而value為待保存的對象。至于為什么是弱引用,大家自己去google下。
??繼續說getMap()方法。getMap()方法很簡單:返回當前線程中的ThreadLocalMap對象。
- 獲取到ThreadLocalMap對象后,如果它不為空。則往該對象里面塞入一個鍵值對,key為ThreadLocal對象的弱引用,vaule為需要保存的對象。
- 如果ThreadLocalMap對象為null,則調用createMap()方法。
??createMap()方法在這里不細說。它會初始化當前線程的ThreadLocalMap對象,并將當前需要保存的對象放入ThreadLocalMap中,key值為當前線程的弱引用對象。
??小結一下:ThreadLocal的set方法會獲取當前線程的ThreadLocalMap對象,如果TreadLocalMap對象不為空,則將當前線程的弱引用作用key,待保存對象作為value保存起來;若ThreadLocalMap對象為null,則會先初始化,再放入鍵值對。
4.2 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對象。那是自然,因為我們上面不就是往ThreadLocalMap里面放的嗎!還記得ThreadLocalMap里面存了啥不?不記得的往上翻一翻。
??如果ThreadLocalMap對象不為空,當前線程作為key值,從ThreadLocalMap中取出來了一個ThreadLocalMap.Entry對象。這個getEntry()方法我們在上面已經已經介紹過了,可能再返回去看看。當然了,肯定有人要問!我們剛才放的時候放的明明不是ThreadLocalMap.Entry對象!這咋回事呢?
??實際上,在ThreadLocalMap中有一個靜態類,它名叫Entry,繼承了WeakReference類。再看看Entry的構造方法。如果調用Entry的get方法,實際上拿到的是ThreadLocal對象的弱引用對象。是不是很熟悉?上面的set方法有聊到過。
??繼續說上面的get()方法。當我們拿到了Entry對象后,如果Entry對象不為空,直接返回Entry對象的value值,即我們想要的值。
那么如果ThreadLocalMap為空呢?則會執行setInitialValue()方法。光看名字,你肯定覺得它無非執行了兩步操作:1.初始化對象;2. 將初始化后的對象塞入ThreadLocalMap對象中;3. 返回初始化后的對象。那我們來看看我們的猜想對不對呢?
??除了第一句代碼,后面的是不是好像都在哪里見過啊?可不是!不就是上面的set()方法嗎?這個方法我們只需要關注initialValue()方法...而這個initialValue()方法是需要我們自己重寫的。
??小結一下:如果ThreadLocalMap中有ThreadLocal對應的值,取Entry對象的value值;如果ThreadLocalMap為null,三步走!初始化,將初始化后的對象放入ThreadLocalMap中,返回初始化后的對象。
5 ThreadLocal總結
??很感謝您能耐心的看到這里,我們最后再總結一下ThreadLoacl的實現機制。
??在Thread類中存在一個ThreaLocalMap變量,ThreadLocalMap中又有一個Entry類型的數組,而這個Entry對象則以ThreadLocal的弱引用為key。當我們調用ThreadLocal的get()方法時,會先獲取當前線程的ThreadLocalMap對象,并將當前ThreadLocal對象作為key(實際上key為ThreadLocal的弱引用),去它的Entry數組中尋找我們需要的value。就這是我們說ThreadLocal為每個線程創建了一個變量副本的意思,線程對自己ThreadLocalMap中的值進行操作時,并不會對其它線程造成影響。
轉載于:https://www.cnblogs.com/cfyrwang/p/8166369.html
總結
以上是生活随笔為你收集整理的Java并发编程之ThreadLocal源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入C#数据类型
- 下一篇: Executors线程池关闭时间计算