ThreadLocal原理介紹及應用場景
本次給大傢介紹重要的工具ThreadLocal
。講解內容如下,同時介紹什麼場景下發生內存泄漏,如何復現內存泄漏,如何正確使用它來避免內存泄漏。
ThreadLocal
是什麼?有哪些用途?ThreadLocal
如何使用ThreadLocal
原理ThreadLocal
使用有哪些坑及註意事項
1. ThreadLocal是什麼?有哪些用途?
首先介紹Thread
類中屬性threadLocals:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
我們發現Thread並沒有提供成員變量threadLocals的設置與訪問的方法,那麼每個線程的實例threadLocals參數我們如何操作呢?這時我們的主角:ThreadLocal就登場瞭。
所以有那麼一句總結:ThreadLocal
是線程Thread中
屬性threadLocals的管理者。
也就是說我們對於ThreadLocal
的get, set,remove的操作結果都是針對當前線程Thread實例的threadLocals存,取,刪除操作。類似於一個開發者的任務,產品經理左右不瞭,產品經理隻能通過技術leader來給開發者分配任務。下面再舉個栗子,進一步說明他們之間的關系:
1.每個人都一張銀行卡
2.每個人每張卡都有一定的餘額。
3.每個人獲取銀行卡餘額都必須通過該銀行的管理系統。
4.每個人都隻能獲取自己卡持有的餘額信息,他人的不可訪問。
映射到我們要說的ThreadLocal
- 1.card類似於Thread
- 2.card餘額屬性,卡號屬性等類似於Treadlocal內部屬性集合threadLocals
- 3.cardManager類似於ThreadLocal管理類
那ThreadLocal有哪些應用場景呢?
其實我們無意間已經時時刻刻在使用ThreadLocal提供的便利,如果說多數據源的切換你比較陌生,那麼spring提供的聲明式事務就再熟悉不過瞭,我們在研發過程中無時無刻不在使用,而spring聲明式事務的重要實現基礎就是ThreadLocal,隻不過大傢沒有去深入研究spring聲明式事務的實現機制。後面有機會我會給大傢介紹spring聲明式事務的原理及實現機制。
原來ThreadLocal
這麼強大,但應用開發者使用較少,同時有些研發人員對於ThreadLocal
內存泄漏,等潛在問題,不敢試用,恐怕這是對於ThreadLocal
最大的誤解,後面我們將會仔細分析,隻要按照正確使用方式,就沒什麼問題。如果ThreadLocal存在問題,豈不是spring聲明式事務是我們程序最大的潛在危險嗎?
2.ThreadLocal如何使用
為瞭更直觀的體會ThreadLocal
的使用我們假設如下場景
- 1.我們給每個線程生成一個ID。
- 2.一旦設置,線程生命周期內不可變化。
- 3.容器活動期間不可以生成重復的ID
我們創建一個ThreadLocal管理類:
測試程序如下:我們同一個線程不斷get,測試id是否變化,同時測試完成後我們就將其釋放掉。
在主程序中我們開啟多個線程測試不通線程之間是否會影響
不出意外我們的結果為:
結果:確實是不同線程間id不同,相同線程id相同。
3.ThreadLocal原理
①ThreadLocal類結構及方法解析:
上圖可知:ThreadLocal
三個方法get, set , remove以及內部類`ThreadLocalMap
②ThreadLocal及Thread之間的關系:
從這張圖我們可以直觀的看到Thread中屬性threadLocals,作為一個特殊的Map,它的key值就是我們ThreadLocal
實例,而value值這是我們設置的值。
③ThreadLocal的操作過程:
我們以get方法為例:
其中getMap(t)返回的就上當前線程的threadlocals,如下圖,然後根據當前ThreadLocal實例對象作為key獲取ThreadLocalMap中的value,如果首次進來這調用setInitialValue()
set的過程也類似:
註意:ThreadLocal
中可以直接t.threadLocals
是因為Thread
與ThreadLocal
在同一個包下,同樣Thread可以直接訪問ThreadLocal.ThreadLocalMap threadLocals = null;
來進行聲明屬性。
4.ThreadLocal使用有哪些坑及註意事項
我經常在網上看到駭人聽聞的標題,ThreadLocal
導致內存泄漏,這通常讓一些剛開始對ThreadLocal
理解不透徹的開發者,不敢貿然使用。越不用,越陌生。這樣就讓我們錯失瞭更好的實現方案,所以敢於引入新技術,敢於踩坑,才能不斷進步。
我們來看下為什麼說ThreadLocal
會引起內存泄漏,什麼場景下會導致內存泄漏?
先回顧下什麼叫內存泄漏,對應的什麼叫內存溢出
- ①Memory overflow:內存溢出,沒有足夠的內存提供申請者使用。
- ②Memory leak:內存泄漏,程序申請內存後,無法釋放已申請的內存空間,內存泄漏的堆積終將導致內存溢出。
顯然是TreadLocal在不規范使用的情況下導致瞭內存沒有釋放。
紅框裡我們看到瞭一個特殊的類WeakReference
,同樣這個類,應用開發者也同樣很少使用,這裡簡單介紹下吧
類型 | 回收時間 | 應用場景 |
---|---|---|
強引用 | 一直存活,除非GC Roots不可達 | 所有程序的場景,基本對象,自定義對象等 |
軟引用 | 內存不足時會被回收 | 一般用在對內存非常敏感的資源上,用作緩存的場景比較多,例如:網頁緩存、圖片緩存 |
弱引用 | 隻能存活到下一次GC前 | 生命周期很短的對象,例如ThreadLocal中的Key。 |
虛引用 | 隨時會被回收, 創建瞭可能很快就會被回收 | 可能被JVM團隊內部用來跟蹤JVM的垃圾回收活動 |
既然WeakReference
在下一次gc即將被回收,那麼我們的程序為什麼沒有出問題呢?
- ①所以我們測試下弱引用的回收機制:
這一種存在強引用不會被回收。
這裡沒有強引用將會被回收。
上面演示瞭弱引用的回收情況,下面我們看下ThreadLocal的弱引用回收情況。
- ②
ThreadLocal
的弱引用回收情況
如上圖所示,我們在作為key的ThreadLocal對象沒有外部強引用,下一次gc必將產生key值為null的數據,若線程沒有及時結束必然出現,一條強引用鏈
Threadref–>Thread–>ThreadLocalMap–>Entry,所以這將導致內存泄漏。
下面我們模擬復現ThreadLocal導致內存泄漏:
1.為瞭效果更佳明顯我們將我們的treadlocals的存儲值value設置為1萬字符串的列表:
class ThreadLocalMemory { // Thread local variable containing each thread's ID public ThreadLocal<List<Object>> threadId = new ThreadLocal<List<Object>>() { @Override protected List<Object> initialValue() { List<Object> list = new ArrayList<Object>(); for (int i = 0; i < 10000; i++) { list.add(String.valueOf(i)); } return list; } }; // Returns the current thread's unique ID, assigning it if necessary public List<Object> get() { return threadId.get(); } // remove currentid public void remove() { threadId.remove(); } }
測試代碼如下:
public static void main(String[] args) throws InterruptedException { // 為瞭復現key被回收的場景,我們使用臨時變量 ThreadLocalMemory memeory = new ThreadLocalMemory(); // 調用 incrementSameThreadId(memeory); System.out.println("GC前:key:" + memeory.threadId); System.out.println("GC前:value-size:" + refelectThreadLocals(Thread.currentThread())); // 設置為null,調用gc並不一定觸發垃圾回收,但是可以通過java提供的一些工具進行手工觸發gc回收。 memeory.threadId = null; System.gc(); System.out.println("GC後:key:" + memeory.threadId); System.out.println("GC後:value-size:" + refelectThreadLocals(Thread.currentThread())); // 模擬線程一直運行 while (true) { } }
此時我們如何知道內存中存在memory leak呢?
我們可以借助jdk提供的一些命令dump當前堆內存,命令如下:
jmap -dump:live,format=b,file=heap.bin <pid>
然後我們借助MAT可視化分析工具,來查看對內存,分析對象實例的存活狀態:
首先打開我們工具提示我們的內存泄漏分析:
這裡我們可以確定的是ThreadLocalMap實例的Entry.value是沒有被回收的。
最後我們要確定Entry.key是否還在?打開Dominator Tree,搜索我們的ThreadLocalMemory,發現並沒有存活的實例。
以上我們復現瞭ThreadLocal
不正當使用,引起的內存泄漏。demo在這裡。
所以我們總結瞭使用ThreadLocal
時會發生內存泄漏的前提條件:
- ①
ThreadLocal
引用被設置為null,且後面沒有set,get,remove操作。 - ②線程一直運行,不停止。(線程池)
- ③觸發瞭垃圾回收。(Minor GC或Full GC)
我們看到ThreadLocal
出現內存泄漏條件還是很苛刻的,所以我們隻要破壞其中一個條件就可以避免內存泄漏,單但為瞭更好的避免這種情況的發生我們使用ThreadLocal時遵守以下兩個小原則:
- ①ThreadLocal申明為private static final。
Private與final 盡可能不讓他人修改變更引用,
Static 表示為類屬性,隻有在程序結束才會被回收。 - ②ThreadLocal使用後務必調用remove方法。
最簡單有效的方法是使用後將其移除。
到此這篇關於ThreadLocal原理介紹及應用場景的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。
推薦閱讀:
- Java ThreadLocal類使用詳解
- 詳解Java中ThreadLocal類型及簡單用法
- Java面試必問之ThreadLocal終極篇分享
- Java ThreadLocal的詳細解釋
- 詳解Java中的ThreadLocal