您好,登錄后才能下訂單哦!
本篇內容介紹了“ThreadLocal的原理是什么”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
JDK1.2
提供的的一個線程綁定變量的類。
他的思想就是:給每一個使用到這個資源的線程都克隆一份,實現了不同線程使用不同的資源,且該資源之間相互獨立
思考一個場景:數據庫連接的時候,我們會創建一個Connection
連接,讓不同的線程使用。這個時候就會出現多個線程爭搶同一個資源的情況。
這種多個線程爭搶同一個資源的情況,很常見,我們常用的解決辦法也就兩種:空間換時間,時間換空間
沒有辦法,魚與熊掌不可兼得也。就如我們的CAP
理論,也是犧牲其中一項,保證其他兩項。
而針對上面的場景我們的解決辦法如下:
空間換時間:為每一個線程創建一個連接。
直接在線程工作中,創建一個連接。(重復代碼太多)
使用ThreadLocal
,為每一個線程綁定一個連接。
時間換空間:對當前資源加鎖,每一次僅僅存在一個線程可以使用這個連接。
通過ThreadLocal
為每一個線程綁定一個指定類型的變量,相當于線程私有化
ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); threadLocal.get(); threadLocal.set(1); threadLocal.remove();
沒錯,這四行代碼已經把ThreadLocal
的使用方法表現得明明白白。
get
從ThreadLocal
拿出一個當前線程所擁有得對象
set
給當前線程綁定一個對象
remove
將當前線程綁定的當前對象移除
記住在使用的以后,一定要remove,一定要remove,一定要remove
為什么要remove
。相信不少小伙伴聽到過ThreadLocal
會導致內存泄漏問題。
沒錯,所以為了解決這種情況,所以你懂吧,用完就移除,別浪費空間(渣男欣慰)
看到這,腦袋上有好多問號出現了(小朋友你是否有很多問號?)
為啥會引發內存泄漏?
為啥不remove就內存泄漏了
它是怎么講對象和線程綁定的
為啥get的時候拿到的就是當前線程的而不是其他線程的
它怎么實現的???
來吧,開淦,源碼來
先來說一個思路:如果我們自己寫一個ThreadLocal
會咋寫?
線程綁定一個對象。**這難道不是我們熟知的map
映射?**有了Map
我們就可以以線程為Key
,對象為value
添加到一個集合中,然后各種get,set,remove
操作,想怎么玩就怎么玩,搞定。????
這個時候,有兄弟說了。你這思路不對啊,你這一個線程僅僅只能存放一個類型的變量,那我想存多個呢?
摸摸自己充盈的發量,你說出了一句至理名言:萬般問題,皆系于源頭和結果之中。
從結果考慮,讓開發者自己搞線程私有(估計被會開發者罵死)
來吧,從源頭考慮。現在我們的需求是:線程可以綁定多個值,而不僅僅是一個。嗯,沒錯,兄弟們把你們的想法說出來。
讓線程自己維護一個Map,將這個ThreadLocal
作為Key
,對象作為Value
不就搞定了
兄弟,牛掰旮旯四
此時,又有兄弟說了。按照你這樣的做法,將ThreadLocal
扔到線程本身的的Map里,那豈不是這個ThreadLocal
一直被線程對象引用,所以在線程銷毀之前都是可達的,都無法GC
呀,有BUG
啊???
**好,問題。**這樣想,既然由于線程和ThreadLocal
對象存在引用,導致無法GC
,那我將你和線程之間的引用搞成弱引用或者軟引用不就成了。一GC
你就沒了。
啥,你不知道啥是弱引用和軟引用???
前面講過的東西,算啦再給你們復習一波。
JDK
中存在四種類型引用,默認是強引用,也就是我們經常干的事情。瘋狂new,new,new
。這個時候創建的對象都是強引用。
強引用。直接new
軟引用。通過SoftReference
創建,在內存空間不足的時候直接銷毀,即它可能最后的銷毀地點是在老年區
弱引用。通過WeakReference
創建,在GC
的時候直接銷毀。即其銷毀地點必定為伊甸區
虛引用。通過PhantomReference
創建,它和不存也一樣,非常虛,只能通過引用隊列在進行一些操作,主要用于堆外內存回收
好了,回到正題,上面的引用里最適合我們當前的場景的就是弱引用了,為什么這個樣子說:
在以往我們使用完對象以后等著GC
清理,但是對于ThreadLocal
來說,即使我們使用結束,也會因為線程本身存在該對象的引用,處于對象可達狀態,垃圾回收器無法回收。這個時候當ThreadLocal
太多的時候就會出現內存泄漏的問題。
而我們將ThreadLocal
對象的引用作為弱引用,那么就很好的解決了這個問題。當我們自己使用完ThreadLocal
以后,當GC
的時候就會將我們創建的強引用直接干掉,而這個時候我們完全可以將線程Map
中的引用干掉,于是使用了弱引用,這個時候大家應該懂了為啥不使用軟引用了吧
還有一個問題:為什么會引發內存泄漏呢?
了解Map
結構的兄弟們應該清楚,內部實際就一個節點數組,對于ThreadLocalMap
而言,內部是一個Entity
,它將Key
作為弱引用,Value
還是強引用。如果我們在使用完ThreadLocal
以后,沒有對Entity
進行移除,會引發內存泄漏問題。
ThreadLocalMap
提供了一個方法expungeStaleEntry
方法用來排除無效的Entity
(Key
為空的實體)
說到這里,有一個問題我思考了蠻久的,value為啥不搞成弱引用,用完直接扔了多好
最后思考出來得答案(按照源碼推了一下):
不設置為弱引用,是因為不清楚這個Value
除了map
的引用還是否還存在其他引用,如果不存在其他引用,當GC
的時候就會直接將這個Value干掉了,而此時我們的ThreadLocal
還處于使用期間,就會造成Value為null的錯誤,所以將其設置為強引用。
而為了解決這個強引用的問題,它提供了一種機制就是上面我們說的將Key
為Null
的Entity
直接清除
到這里,這個類的設計已經很清楚了。接下來我們看一下源碼吧!
需要注意的一個點是:ThreadLocalMap
解決哈希沖突的方式是線性探測法。
人話就是:如果當前數組位有值,則判斷下一個數組位是否有值,如果有值繼續向下尋找,直到一個為空的數組位
Set方法
class ThreadLocal public void set(T value) { //拿到當前線程 Thread t = Thread.currentThread(); //獲取當前線程的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) //如果當前線程的Map已經創建,直接set map.set(this, value); else //沒有創建,則創建Map createMap(t, value); } private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //拿到當前數組位,當前數組位是否位null,如果為null,直接賦值,如果不為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; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; //清除一些失效的Entity if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } ThreadLocalMap getMap(Thread t) { //獲取當前線程的ThreadLocalMap return t.threadLocals; } void createMap(Thread t, T firstValue) { //當前對象作為Key,和我們的設想一樣 t.threadLocals = new ThreadLocalMap(this, firstValue); }
Get方法
public T get() { //獲取當前線程 Thread t = Thread.currentThread(); //拿到當前線程的Map 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; } } return setInitialValue(); } private Entry getEntry(ThreadLocal<?> key) { //計算數組位 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; //如果當前數組有值,且數組位的key相同,則返回value if (e != null && e.get() == key) return e; else //線性探測尋找對應的Key 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) //排除當前為空的Entity expungeStaleEntry(i); else //獲取下一個數組位 i = nextIndex(i, len); e = tab[i]; } //如果沒有找到直接返回空 return null; }
remove
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(); //清空位NUll的實體 expungeStaleEntry(i); return; } } }
我們可以看到一個現象:在set
,get
,remove
的時候都調用了expungeStaleEntry
來將所有失效的Entity
移除
看一下這個方法做了什么
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 刪除實體的Value tab[staleSlot].value = null; //置空這個數組位 tab[staleSlot] = null; //數量減一 size--; // 重新計算一次哈希,如果當前數組位不為null,線性查找直到一個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; }
“ThreadLocal的原理是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。