天天看點

詳解ThreadLocal的設計思想和擴容機制

詳解ThreadLocal的設計思想和擴容機制

一、什麼是ThreadLocal

對于共享資源,在多線程環境下勢必會存線上程安全的問題。主要有兩種方式可以處理這個問題,一種,就是将資源處理變成單線程,同一時刻隻能有一個線程持有,比如加鎖。另一種,用空間換時間的思想,将共享資源變成非共享,比如每個線程都各自持有一份,線程隻操作自己持有的資源,那樣就不存線上程安全的問題。ThreadLocal就是采用了第二種思想實作。

​将一個共享資源定義成ThreadLocal,JDK1.8 ThreadLocal提供了withInitial很友善的去聲明一個線程的局部變量,以SimpleDateFormat為例,具體用法:

二、源碼解讀

先看一下Thread類,内部定義了一個ThreadLocal.ThreadLocalMap,用來存放線程自身持有的變量。檢視源碼,可以看到對于ThreadLocalMap的定義:存放與該線程有關的ThreadLocal值,這個Map是由ThreadLocal去維護。可見Thread類隻是定義了這麼一個東西,但是卻不去維護裡面的值,有點不負責任的樣子。

詳解ThreadLocal的設計思想和擴容機制

回到ThreadLocal這個類,對于Map的維護無非就是資料的寫入和讀取。從get()可以看到,會先擷取目前的線程,從目前線程拿到ThreadLocalMap,從Map中擷取目前的ThreadLocal對象(以上面為例,就是擷取LOCAL_FORMAT),如果不為空就傳回對應的值,如果Map是空或者擷取不到目前ThreadLocal對象,那麼就會進行初始化。

詳解ThreadLocal的設計思想和擴容機制

setInitialValue()裡同樣去擷取目前線程裡面的Map,如果沒有,調用createMap去初始化Map。

詳解ThreadLocal的設計思想和擴容機制

繼續跟蹤源碼,可以看到最後調用的是ThreadLocalMap的構造函數,建立了一個Entry數組,初始值是16,初始大小是1,設定門檻值是目前大小的2/3,同時計算了第一個元素的位置。

詳解ThreadLocal的設計思想和擴容機制

再看一下set()方法,也是擷取目前線程的ThreadLocalMap,如果不為空,就調用ThreadLocalMap.set()添加目前的值,如果為空,則去建立Map。

詳解ThreadLocal的設計思想和擴容機制

對于ThreadLocalMap.set()方法,通過key和hashCode去計算目前存放的位置,如果目前位置位置已存在元素,則判斷key是否相同,相同則覆寫,不相同判斷是否為null,如果為null,則說明原先所在的key被回收了,此時會替換舊值。如果找不到,則說明目前位置已經被其他人占用了,則會往後繼續找合适的位置存放。其實就是開放尋址的思想。

詳解ThreadLocal的設計思想和擴容機制

三、ThreadLocalMap擴容機制

根據上圖可以看到,添加了元素後,會判斷目前下标往後的元素是否存在需要value回收的,如果不存在需要回收并且目前總元素大于等于門檻值,那麼就會調用rehash()擴容。擴容之前還會再重新對key已經被GC了的元素進行回收,回收後如果總數還是大于等于門檻值的3/4,那麼就會調用resize()進行真正的擴容。

詳解ThreadLocal的設計思想和擴容機制

檢視擴容源碼,可以看到擴容後的數組是原來數組的2倍,原有的元素會重新進行hash計算放到新的數組裡面,處理完後還會重置門檻值。

詳解ThreadLocal的設計思想和擴容機制

四、存在的問題

ThreadLocal解決了多線程下共享資源并發的問題,但也存在其他問題。

看一下Entry的結構,繼承了WeakReference,可見ThreadLocalMap的key是弱引用。在JVM裡弱引用生命周期是比較短的,JVM掃描一旦發現有弱引用,無論目前記憶體空間是否足夠,都會進行回收。而ThreadLocal的value是強引用,這樣一來key會被清除掉,而value不會被清除掉,如果不做任何處理,value永遠無法被回收,這個時候就可能會産生記憶體洩露。

詳解ThreadLocal的設計思想和擴容機制

而ThreadLocal也考慮到了這個問題,是以在get()、set()、remove()方法裡都提供對key為null資料的處理,最終都是調用expungeStaleEntry進行重新散列來清除key為空的資料。

詳解ThreadLocal的設計思想和擴容機制

是以最好每次使用後都調用remove()方法。其實也在想,為什麼ThreadLocalMap的key不定義成強引用,那樣就可以減少維護Map的代碼。後來想了下,這裡可能也是為了實作即使沒有顯示調用remove()也可以回收不怎麼經常被用到對象,畢竟如果線程比較多的話,相當于會有多個相同的對象存在記憶體裡,如果忘記顯示調用remove(),對象常駐記憶體也是種壓力,更多算是一種兜底的考慮吧。