ThreadLocal全攻略:使用實戰(zhàn),源碼分析,內(nèi)存泄露分析
前言
說起ThreadLocal即便你沒有直接用到過,它也間接的出現(xiàn)在你使用過的框架里,比如Spring的事物管理,Hibernate的Session管理、logback(和log4j)中的MDC功能實現(xiàn)等。而在項目開發(fā)中,比如用到的一些分頁功能的實現(xiàn)往往也會借助于ThreadLocal。
正是因為ThreadLocal的無處不在,所以在面試的時候也經(jīng)常會被問到它的實現(xiàn)原理、核心API使用以及內(nèi)存泄露的問題。
而且基于這些問題還可以拓展到線程安全方面、JVM內(nèi)存管理與分析、Hash算法等等知識點。可見ThreadLocal對開發(fā)人員來說是多么的重要的。如果你還沒有全面的了解,那么這篇文章值得你深入學習一下。
什么是ThreadLocal
ThreadLocal是Therad的局部變量的維護類,在Java中是作為一個特殊的變量存儲在。當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
因為每個Thread內(nèi)有自己的實例副本,且該副本只能由當前Thread使用,也就不存在多線程間共享的問題。
總的來說,ThreadLocal適用于每個線程需要自己獨立的實例且該實例需要在多個方法中被使用,也即變量在線程間隔離而在方法或類間共享的場景。
比如,有一個變量count,在多線程并發(fā)時操作count++會出現(xiàn)線程安全問題。但是通過ThreadLocal就可以為每個線程創(chuàng)建只屬于當前線程的count副本,各自操作各自的副本,不會影響到其他線程。
從另外一個角度來說,ThreadLocal是一個數(shù)據(jù)結構,有點像HashMap,可以保存"key:value"鍵值對,但是一個ThreadLocal只能保存一個鍵值對,各個線程的數(shù)據(jù)互不干擾。
- @Test
- public void test1(){
- ThreadLocal<String> localName = new ThreadLocal<>();
- // 只提供了一個set方法;
- localName.set("程序新視界");
- // 同時只提供了一個get方法
- String name = localName.get();
- System.out.println(name);
- }
上述代碼中線程A初始化了一個ThreadLocal對象,并調用set方法,保持了一個值。而這個值只能線程A調用get方法才能獲取到。如果此時線程B調用get方法是無法獲取到的。至于如何實現(xiàn)這一功能的,我們在后面源代碼分析中進行講解,這里知道其功能即可。
ThreadLocal使用實例
上面介紹了使用場景和基本的實現(xiàn)理論,下面我們就來通過一個簡單的實例看一下如何使用ThreadLocal。
- public class ThreadLocalMain {
- /**
- * ThreadLocal變量,每個線程都有一個副本,互不干擾
- */
- public static final ThreadLocal<String> HOLDER = new ThreadLocal<>();
- public static void main(String[] args) throws Exception {
- new ThreadLocalMain().execute();
- }
- public void execute() throws Exception {
- // 主線程設置值
- HOLDER.set("程序新視界");
- System.out.println(Thread.currentThread().getName() + "線程ThreadLocal中的值:" + HOLDER.get());
- new Thread(() -> {
- System.out.println(Thread.currentThread().getName() + "線程ThreadLocal中的值:" + HOLDER.get());
- // 設置當前線程中的值
- HOLDER.set("《程序新視界》");
- System.out.println("重新設置之后," + Thread.currentThread().getName() + "線程ThreadLocal中的值:" + HOLDER.get());
- System.out.println(Thread.currentThread().getName() + "線程執(zhí)行結束");
- }).start();
- // 等待所有線程執(zhí)行結束
- Thread.sleep(1000L);
- System.out.println(Thread.currentThread().getName() + "線程ThreadLocal中的值:" + HOLDER.get());
- }
- }
示例中定義了一個static final的ThreadLocal變量HOLDER,在main方法中模擬通過兩個線程來操作HOLDER中存儲的值。先對HOLDER設置一個值,然后打印獲取得到的值,然后新起一個線程去修改HOLDER中的值,然后分別在新線程和主線程兩處獲取對應的值。
執(zhí)行程序,打印結果如下:
- main線程ThreadLocal中的值:程序新視界
- Thread-0線程ThreadLocal中的值:null
- 重新設置之后,Thread-0線程ThreadLocal中的值:《程序新視界》
- Thread-0線程執(zhí)行結束
- main線程ThreadLocal中的值:程序新視界
對照程序和輸出結果,你會發(fā)現(xiàn),主線程和Thread-0各自獨享自己的變量存儲。主線程并沒有因為Thread-0調用了HOLDER的set方法而被改變。
之所以能達到這個效果,正是因為在ThreadLocal中,每個線程Thread擁有一份自己的副本變量,多個線程互不干擾。那么,你會疑惑,ThreadLocal是如何實現(xiàn)這一功能的呢?
ThreadLocal原理分析
在學習ThreadLocal的原理之前,我們先來看一些相關的理論知識和數(shù)據(jù)結構。
基本流程與源碼實現(xiàn)
一個線程內(nèi)可以存多個ThreadLocal對象,存儲的位置位于Thread的ThreadLocal.ThreadLocalMap變量,在Thread中有如下變量:
- /* ThreadLocal values pertaining to this thread. This map is maintained
- * by the ThreadLocal class. */
- ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap是由ThreadLocal維護的靜態(tài)內(nèi)部類,正如代碼中注解所說這個變量是由ThreadLocal維護的。
我們在使用ThreadLocal的get()、set()方法時,其實都是調用了ThreadLocalMap類對應的get()、set()方法。
Thread中的這個變量的初始化通常是在首次調用ThreadLocal的get()、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);
- }
上述set方法中,首先獲取當前線程對象,然后通過getMap方法來獲取當前線程中的threadLocals:
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
如果Thread中的對應屬性為null,則創(chuàng)建一個ThreadLocalMap并賦值給Thread:
- void createMap(Thread t, T firstValue) {
- t.threadLocals = new ThreadLocalMap(this, firstValue);
- }
如果已經(jīng)存在,則通過ThreadLocalMap的set方法設置值,這里我們可以看到set中key為this,也就是當前ThreadLocal對象,而value值則是我們要存的值。
對應的get方法源碼如下:
- public T get() {
- Thread t = Thread.currentThread();
- 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();
- }
可以看到同樣通過當前線程,拿到當前線程的threadLocals屬性,然后從中獲取存儲的值并返回。在get的時候,如果Thread中的threadLocals屬性未進行初始化,則也會間接調用createMap方法進行初始化操作。
下面我們通過一個流程圖來匯總一下上述流程:
上述流程中給Thread的threadLocals屬性初始化的操作,在JDK8和9中通過debug發(fā)現(xiàn),都沒有走createMap方法,暫時還不清楚JVM是如何進行初始化賦值的。而在測試JDK13和JDK14的時候,很明顯走了createMap方法。
ThreadLoalMap的數(shù)據(jù)結構
ThreadLoalMap是ThreadLocal中的一個靜態(tài)內(nèi)部類,類似HashMap的數(shù)據(jù)結構,但并沒有實現(xiàn)Map接口。
ThreadLoalMap中初始化了一個大小16的Entry數(shù)組,Entry對象用來保存每一個key-value鍵值對。通過上面的set方法,我們已經(jīng)知道其中的key永遠都是ThreadLocal對象。
看一下相關的源碼:
- static class ThreadLocalMap {
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
- private static final int INITIAL_CAPACITY = 16;
- // ...
- }
ThreadLoalMap的類圖結構如下:
這里需要留意的是,ThreadLocalMap類中的Entry對象繼承自WeakReference,也就是說它是弱引用。這里會出現(xiàn)內(nèi)存泄露的情況,后續(xù)會講到。
由于hreadLocalMaps是延遲創(chuàng)建的,因此在構造時至少要創(chuàng)建一個Entry對象。這里可以從構造方法中看到:
- 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);
- }
上述構造方法,創(chuàng)建了一個默認長度為16的Entry數(shù)組,通過hashCode與length位運算確定索引值i。而上面也提到,每個Thread都有一個ThreadLocalMap類型的變量。
至此,結合Thread,我們可以看到整個數(shù)據(jù)模型如下:
hash沖突及解決
我們留意到構造方法中Entry在table中存儲位置是通過hashcode算法獲得。每個ThreadLocal對象都有一個hash值threadLocalHashCode,每初始化一個ThreadLocal對象,hash值就增加一個固定的大小0x61c88647。
在向ThreadLocalMap中的Entry數(shù)值存儲Entry對象時,會根據(jù)ThreadLocal對象的hash值,定位到table中的位置i。這里分三種情況:
- 如果當前位置為空的,直接將Entry存放在對應位置;
- 如果位置i已經(jīng)有值且這個Entry對象的key正好是即將設置的key,那么重新設置Entry中的value;
- 如果位置i的Entry對象和即將設置的key沒關系,則尋找一個空位置;
計算hash值便會有hash沖突出現(xiàn),常見的解決方法有:再哈希法、開放地址法、建立公共溢出區(qū)、鏈式地址法等。
上面的流程可以看出這里采用的是開放地址方法,如果當前位置有值,就繼續(xù)尋找下一個位置,注意table[len-1]的下一個位置是table[0],就像是一個環(huán)形數(shù)組,所以也叫閉散列法。如果一直都找不到空位置就會出現(xiàn)死循環(huán),發(fā)生內(nèi)存溢出。當然有擴容機制,一般不會找不到空位置的。
ThreadLocal內(nèi)存泄露
ThreadLocal使用不當可能會出現(xiàn)內(nèi)存泄露,進而可能導致內(nèi)存溢出。下面我們就來分析一下內(nèi)存泄露的原因及相關設計思想。
內(nèi)存引用鏈路
根據(jù)前面對ThreadLocal的分析,得知每個Thread維護一個ThreadLocalMap,它key是ThreadLocal實例本身,value是業(yè)務需要存儲的Object。也就是說ThreadLocal本身并不存儲值,它只是作為一個key來讓線程從ThreadLocalMap獲取value。
仔細觀察ThreadLocalMap,這個map是使用ThreadLocal的弱引用作為Key的,弱引用的對象在GC時會被回收。因此使用了ThreadLocal后,引用鏈如圖所示:
其中虛線表示弱引用。下面我們先來了解一下Java中引用的分類。
Java中的引用
Java中通常會存在以下類型的引用:強引用、弱引用、軟引用、虛引用。
- 強引用:通常new出來的對象就是強引用類型,只要引用存在,垃圾回收器將永遠不會回收被引用的對象,哪怕內(nèi)存不足的時候;
- 軟引用:使用SoftReference修飾的對象被稱為軟引用,軟引用指向的對象在內(nèi)存要溢出的時候被回收。如果回收之后,還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常;
- 弱引用:使用WeakReference修飾的對象被稱為弱引用,只要發(fā)生垃圾回收,無論當前內(nèi)存是否足夠,都會回收掉只被弱引用關聯(lián)的對象實例。
- 虛引用:虛引用是最弱的引用,在Java中使用PhantomReference進行定義。虛引用中唯一的作用就是用隊列接收對象即將死亡的通知。
泄露原因分析
正常來說,當Thread執(zhí)行完會被銷毀,Thread.threadLocals指向的ThreadLocalMap實例也隨之變?yōu)槔?,它里面存放的Entity也會被回收。這種情況是不會發(fā)生內(nèi)存泄漏的。
發(fā)生內(nèi)存泄露的場景一般存在于線程池的情況下。此時,Thread生命周期比較長(存在循環(huán)使用),threadLocals引用一直存在,當其存放的ThreadLocal被回收(弱引用生命周期比較短)后,對應的Entity就成了key為null的實例,但value值不會被回收。如果此Entity一直不被get()、set()、remove(),就一直不會被回收,也就發(fā)生了內(nèi)存泄漏。
所以,通常在使用完ThreadLocal后需要調用remove()方法進行內(nèi)存的清除。
比如在web請求當中,我們可以通過過濾器等進行回收方法的調用:
- public void doFilter(ServeletRequest request, ServletResponse){
- try{
- //設置ThreadLocal變量
- localName.set("程序新視界");
- chain.doFilter(request, response)
- }finally{
- //調用remove方法溢出threadLocal中的變量
- localName.remove();
- }
- }
為什么使用弱引用而不是強引用?
從表面上看內(nèi)存泄漏的根源在于使用了弱引用,但為什么JDK采用了弱引用的實現(xiàn)而不是強引用呢?
先來看ThreadLocalMap類上的一段注釋:
- To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
- 為了協(xié)助處理數(shù)據(jù)比較大并且生命周期比較長的場景,hash table的條目使用了WeakReference作為key。
為了協(xié)助處理數(shù)據(jù)比較大并且生命周期比較長的場景,hash table的條目使用了WeakReference作為key。
這跟我們想象的有些不同,弱引用反而是為了解決內(nèi)存存儲問題而專門使用的。
我們先來假設一下,如果key使用強引用,那么在其他持有ThreadLocal引用的對象都回收了,但ThreadLocalMap依舊持有ThreadLocal的強引用,這就導致ThreadLocal不會被回收,從而導致Entry內(nèi)存泄露。
對照一下,弱引用的情況。持有ThreadLocal引用的對象都回收了,ThreadLocalMap持有的是ThreadLocal的弱引用,會被自動回收。只不過對應的value值,需要在下次調用set/get/remove方法時會被清除。
綜合對比會發(fā)現(xiàn),采用弱引用反而多了一層保障,ThreadLocal被清理后key為null,對應的value在下一次ThreadLocalMap調用set、get、remove的時候可能會被清除。
所以,內(nèi)存泄露的根本原因是是否手動清除操作,而不是弱引用。
ThreadLocal應用場景
最后,我們再來回顧一下ThreadLocal的應用場景:
- 線程間數(shù)據(jù)隔離,各線程的ThreadLocal互不影響;
- 方便同一個線程使用某一對象,避免不必要的參數(shù)傳遞;
- 全鏈路追蹤中的traceId或者流程引擎中上下文的傳遞一般采用ThreadLocal;
- Spring事務管理器采用了ThreadLocal;
- Spring MVC的RequestContextHolder的實現(xiàn)使用了ThreadLocal;
小結
本篇文章我們從ThreadLocal的使用場景、源碼、類結構、內(nèi)存結構等進行分析說明,最后分析了其引起內(nèi)存泄露的根本原因。通過本篇文章的學習,基本上能掌握ThreadLocal百分之百九十的核心知識點。你學到了嗎?