1、簡介
ThreadLocal也稱線程變量,它是一個以ThreadLocal對象為鍵、任意對象為值的存儲結構(ThreadLocal中ThreadLocalMap的Entry結構),這個結構會被附帶線上程上,以此來做線程資料的隔離。ThreadLocal是維持線程的封閉性的一種規範,它提供set()/get()等方法維護和通路線程中存儲的私有副本,ThreadLocal通常用于防止對可變的單執行個體變量或者全局變量進行共享。
ThreadLocal和synchronized兩者經常會被拿出來一起讨論,雖然二者都是用來解決多線程中資源的通路沖突等問題,但是二者存在本質上的差別具有完全不一樣的使用場景。這裡簡單說明一下:
- synchronized是通過線程阻塞(加鎖),隻允許同步區域内同一時刻隻有一個線程在執行來實作共享資源的互斥通路,犧牲了程式的執行時間
- ThreadLocal是每個線程具有不同的資料副本,通過線程資料隔離互不影響的方式來解決并發資源的通路,犧牲的是存儲空間
相比之下ThreadLocal的使用場景比較特殊,在某些需要以線程為作用域做資源隔離的場景下使用,比如應用程式中以線程為機關發起的資料庫連接配接,可以通過将JDBC的連接配接儲存到ThreadLocal對象中來保證線程安全。
ThreadLocal的簡單使用示例:
package com.liziba.tl;
/**
* <p>
* ThreadLocal demo -> 線程隔離
* </p>
*
* @Author: Liziba
*/
public class ThreadLocalDemo {
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> Integer.valueOf(10));
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> run(), "Thread-" + i).start();
}
}
public static void run() {
ThreadLocalDemo local = new ThreadLocalDemo();
local.threadLocal.set(local.threadLocal.get() + 5);
System.out.println(Thread.currentThread().getName() +" : "+local.threadLocal.get());
}
輸出結果:

上述運作結果可以看到線程并行運作,但線程各自擁有資源副本,彼此之間互不影響,是線程安全的。
2、Thread、ThreadLocal、ThreadLocalMap三者的關系
在進行源碼分析和原理講解之前,有必要先了解這三者之間的關系。Thread、ThreadLocal、ThreadLocalMap這三者從命名都包含一個Thread那麼它們具體是什麼關系呢?接下來通過一些重要的代碼片段和圖示來闡述三者之間的關系,并且也會介紹到ThreadLocal、ThreadLocalMap中的一些重要屬性和資料結構。
java.lang.Thread 中的代碼片段:
public class Thread implements Runnable {
/** Thread中持有一個ThreadLocal中的ThreadLocalMap */
ThreadLocal.ThreadLocalMap threadLocals = null;
java.lang.ThreadLocal中的代碼片段:
public class ThreadLocal<T> {
/**
* ThreadLocalMap 是ThreadLocal的靜态内部類
*/
static class ThreadLocalMap {
// 在下面
java.lang.ThreadLocal.ThreadLocalMap中的代碼片段:
static class ThreadLocalMap {
/** 預設的初始Entry的大小 */
private static final int INITIAL_CAPACITY = 16;
/** 定義一個Entry數組,用來存放多個ThreadLocal */
private Entry[] table;
/** 數組擴容因子 */
private int threshold;
/** 記錄table中Entry的個數 */
private int size = 0;
* ThreadLocalMap中有靜态内部類Entry,Entry繼承了WeakReference弱引用,引用類型是ThreadLocal<?>
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
/**
* key是ThreadLocal對象
* value是ThreadLocal中的value
*/
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
* ThreadLocalMap的構造函數
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化數組,預設16
table = new Entry[INITIAL_CAPACITY];
// 通過一定的算法計算ThreadLocal在table數組中的索引 -> 這個3.2中我做了詳細講解
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 指派table[i]位置Entry對象key -> ThreadLocal | value -> 傳入的值
table[i] = new Entry(firstKey, firstValue);
// 記錄 table中Entry的個數
size = 1;
// 計算擴容因子 16 * 2 / 3
setThreshold(INITIAL_CAPACITY);
看了上述的代碼和代碼的注釋,可以很明确的看到Thread、ThreadLocal、ThreadLocalMap這三者關系
- Thread線程類内部維護了一個ThreadLocalMap成員變量(ThreadLocalMap的執行個體)
- ThreadLocalMap是ThreadLocal的靜态内部類,此外ThreadLocalMap内部維護了一個Entry數組table,用來存放多個ThreadLocal
- ThreadLocal類用于存儲以線程為作用域的資料,用于資料隔離
從這張圖能非常清晰的看出,ThreadLocal隻是ThreadLocalMap操作的一個入口,它提供的set()/get()方法供程式員開發使用,具體的資料存取都是在ThreadLocalMap中去實作,而每一個Thread對象中持有一個ThreadLocalMap對象,不難看出ThreadLocalMap才是實作的關鍵和重難點。
3、ThreadLocal源碼分析
ThreadLocal是JDK提供給程式員直接使用的類,其重點在于ThreadLocalMap,是以下面主要介紹ThreadLocal的關鍵成員屬性、如何通過魔數計算散列均勻的索引、get()/set()方法。重點将在ThreadLocalMap中去介紹。
3.1 ThreadLocal重要成員屬性
ThreadLocal中有幾個重要的成員屬性如下所示:
/** 定義數組的初始大小 */
private static final int INITIAL_CAPACITY = 16;
/** 魔數 -> 可以讓生成出來的值或者說ThreadLocal的Index均勻的分布在2^n的數組大小中 */
private static final int HASH_INCREMENT = 0x61c88647;
/** 魔數 */
private final int threadLocalHashCode = nextHashCode();
/** 定義一個線程安全的原子類AtomicInteger,用于魔數的累加 */
private static AtomicInteger nextHashCode = new AtomicInteger();
nextHashCode()方法:
/** 計算下一個code(魔數累加) */
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
3.2 ThreadLocal計算其在ThreadLocalMap的Entry數組的下标
上面的魔數與斐波拉契散列有關,它可以讓生成出來的值或者說ThreadLocal在table的Index均勻的分布在2^n的數組大小中,我們通過計算的值再取模數組的length-1,就能得到ThreadLocal在ThreadLocalMap的Entry中的索引下标。下面通過自己寫一個測試案例來簡單的講述下這個魔數和計算數組索引:
package com.lizba.currency.threadlocal;
import java.util.concurrent.atomic.AtomicInteger;
* 通過魔數0x61c88647來計算數組索引下标
* @Date: 2021/7/2 22:02
public class ThreadLocal0x61c88647 {
/** 定義數組的初始大小 */
/** 魔數 -> 可以讓生成出來的值或者說ThreadLocal的Index均勻的分布在2^n的數組大小中 */
private static final int HASH_INCREMENT = 0x61c88647;
/** 魔數 */
private final int threadLocalHashCode = nextHashCode();
/** 定義一個線程安全的原子類AtomicInteger,用于魔數的累加 */
private static AtomicInteger nextHashCode = new AtomicInteger();
/** 計算下一個code(魔數累加) */
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
* 根據生成的均勻分布的随機數threadLocalHashCode 取模(%) (數組大小INITIAL_CAPACITY-1(因為數組索引從0開始))
*
* @return
public int index() {
return this.threadLocalHashCode & (INITIAL_CAPACITY - 1);
測試上述代碼:
public static void main(String[] args) {
// 輸出16次,模拟ThreadLocal中的預設初始大小
for (int i = 0; i < 16; i++) {
ThreadLocal0x61c88647 demo = new ThreadLocal0x61c88647();
System.out.println(demo.index());
魔數計算數組索引下标順序圖示:
可以看到運算16次後,數組的16個位置均被插入了一個值,這個就是ThreadLocal中用來計算ThreadLocal在ThreadLocalMap的Entry數組中的索引的方法(非常神奇的一個值,具體算法的探究本文不展開了,作者李子捌的能力也是非常有限)。
3.3 set()方法源碼分析
public void set(T value) {
// 擷取目前線程
Thread t = Thread.currentThread();
// 擷取目前線程的ThreadLocalMap -> getMap(t)方法在下面
ThreadLocalMap map = getMap(t);
// ThreadLocalMap不為空,表示已經初始化 -> 這裡兩個分支是ThreadLocal的重點
if (map != null)
map.set(this, value); // 直接設值,key為目前ThreadLocal對象,value為set傳入的值T
else
createMap(t, value); // 為空則需要初始化,再設值
擷取目前線程的ThreadLocalMap -> getMap(Thread t)
* java.lang.Thread類中定義了ThreadLocal.ThreadLocalMap threadLocals = null;
* t.threadLocals為擷取目前線程對象的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
// 擷取目前線程的ThreadLocalMap
return t.threadLocals;
ThreadLocalMap為空初始化 -> createMap(t, value)
void createMap(Thread t, T firstValue) {
// 執行個體化一個ThreadLocalMap,指派給目前線程的threadLocals成員變量
// new ThreadLocalMap(this, firstValue) -> 源碼分析放到後面ThreadLocalMap中去講解,這裡隻需要明白這是初始化一個ThreadLocalMap即可,加上第二節中三者的說明,也能了解其中的原理。
t.threadLocals = new ThreadLocalMap(this, firstValue);
3.4 get()方法源碼分析
public T get() {
// 擷取目前線程t
// 擷取當選線程的ThreadLocalMap -> 下面貼出了代碼
// 如果不為空
if (map != null) {
// 從ThreadLocalMap的table中取出Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 傳回entry 存儲的值 (entry key為ThreadLocal對象,value為存儲的值)
T result = (T)e.value;
return result;
// 初始化ThreadLocalMap或建構value為null一個entry到table中
// 具體邏輯在下面展示 get() -> setInitialValue()
return setInitialValue();
get() -> getMap(Thread t)方法:
// 擷取當選線程的ThreadLocalMap
get() -> setInitialValue()方法:
* 這個方法于set方法邏輯一緻,隻是初始化的value為null
private T setInitialValue() {
// initialValue()傳回null
T value = initialValue();
// 後續操作與set()方法是完全相同的
// 這個方法是私有的無法被子類重寫 -> 相當于set()方法的一個副本,子類重寫了set()方法,還可以使用這個方法來初始化
map.set(this, value);
createMap(t, value);
return value;
4、ThreadLocalMap源碼分析
ThreadLocalMap是整篇文章的重點,ThreadLocalMap是ThreadLocal的内部類,它提供了真正資料存取的能力;ThreadLocalMap為每個Thread都維護了一個table,這個table中的每一個Entry代表一個ThreadLocal(注意一個線程可以定義多個ThreadLocal,此時它們會存儲在table中不同的下标位置)和vlaue的組合。接下來通過源碼一層層的分析ThreadLocalMap的原理及實作。
4.1 Entry源碼分析
Entry是ThreadLocalMap的靜态内部類,它是一個負責元素存儲的key-value鍵值對資料結構,key是ThreadLocal,value是ThreadLocal傳入的相關的值。這裡有一個重點知識,Entry繼承了WeakReference,是以很明顯的看出ThreadLocal<?> k将會是一個弱引用,弱引用容易被JVM垃圾收集器回收,是以可能導緻記憶體洩露的問題(後續在詳細分析,這裡的重點是ThreadLocalMap的實作)。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
// key -> 是弱引用
super(k);
// 儲存值
value = v;
4.2 ThreadLocalMap構造函數
在ThreadLocal中3.3 set()方法源碼分析中留下來createMap(t, value)的疑問,在擷取線程的ThreadLocalMap為空時,通過調用createMap(t, value)方法對ThreadLocalMap進行了初始化。
// ThreadLocal中set()方法調用的createMap方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)源碼:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 執行個體化一個大小為16的Entry數組,指派給Entry[] table
table = new Entry[INITIAL_CAPACITY];
// 根據目前的ThreadLocal計算其在table中的數組下标,這裡不懂看前面3.2
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 通過傳入的ThreadLocal和value值,構造一個Entry指派給table的指定位置的值
table[i] = new Entry(firstKey, firstValue);
// 記錄table中Entry的個數,也擁有觸發擴容,初始化時為1
size = 1;
// 設定擴容門檻值len * 2 / 3
setThreshold(INITIAL_CAPACITY);
4.3 set()方法源碼分析
在ThreadLocal的set()方法中,當ThreadLocalMap不為空時,也就是說在上面4.2初始化之後,目前線程再次調用ThreadLocal的set()方法将會執行的是下面的邏輯。set()方法中有三個重點知識:
- 當計算的Entry下标位置不存在資料時,直接插入
- 當存在資料時,通過線性探測來解決hash沖突
- 當table中的Entry個數達到擴容門檻值時,進行擴容處理
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 計算數組下标
int i = key.threadLocalHashCode & (len-1);
// 線性探測
// for循環中的内容就是從目前産生hash沖突的位置往後找
// 找到不為null的Entry 有兩種情況 1、key相等則更新 2、key=null則需要做replaceStaleEntry處理
// 如果為null,結束for循環
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 擷取目前節點的key -> ThreadLocal 對象
ThreadLocal<?> k = e.get();
// 如果key相同則直接替換,結束循環
if (k == key) {
e.value = value;
return;
// Entry存在,但是Entry的key為空,表示引用被垃圾回收器回收了
// 此時需要做比較複雜的處理,這個處理請看後面我的詳細分析,此處你可以了解為就是找個能放的索引位置放進去,然後結束循環
if (k == null) {
replaceStaleEntry(key, value, i);
// 在table[i] = null 的位置插入新的entry
tab[i] = new Entry(key, value);
// size + 1
int sz = ++size;
// 如果沒有需求清理的key = null的entry,并且size到達擴容門檻值
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 擴容處理
rehash();
set() -> nextIndex(i, len)方法:
* 這裡是線性探測的思想,一直往後周遊
* 當到達數組的最後一個位置仍未找到滿足條件的,再從數組的最前面開始周遊
*/
private static int nextIndex(int i, int len) {
// 當數組下标不越界的情況下 傳回 i+1 否則傳回 0
return ((i + 1 < len) ? i + 1 : 0);
set() -> replaceStaleEntry(key, value, i)方法:
這個方法非常重要,它負責對過期的entry(引用被垃圾收集器回收了,因為Entry的key是弱引用,前面Entry源碼中有介紹)進行清理,尋找合适的位置插入新的節點、對數組中已有的Entry做rehash尋找新的下标。設計源碼的作者思路主要分為如下兩個方面:
- 向前搜尋,尋找其他同樣key為null被GC的Entry節點,并記錄下最後周遊到的Entry索引,周遊結束條件是Entry為null。這樣的好處是為了清理這些Entry的key被GC了的Entry節點。
- 向後周遊,ThreadLocal不同于hashmap,它是開放位址法,是以目前索引位置不一定就是這個Entry存放的位置,可能第一次存放的時候發生了hash碰撞,Entry的存儲發生了後移,是以要向後周遊,尋找目前與Entry的key相等的槽。
關于replaceStaleEntry(key, value, i)方法,我畫了一個簡圖,圖中并未包含所有場景,具體請詳細閱讀源碼(非常精彩的設計思路),假設進入這個方法時staleSlot = 8,并且key的hashcode = 0xx68
源碼分析:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry e;
// 将目前索引的值指派給slotToExpunge,用于清理
int slotToExpunge = staleSlot;
// 向前搜尋,知道tab[i] == null
// 如果tab[i] 不為空,但是tab[i]的key為空,也就是和目前節點一樣的情況,key被GC了,那麼将目前索引下标的值指派給slotToExpunge,記錄最小的索引值,後續從這裡開始清理
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 向後周遊,直到tab[i]==null
for (int i = nextIndex(staleSlot, len);
i = nextIndex(i, len)) {
// 擷取目前索引位置Entry的key
// 如果key相等,證明目前這個節點後移到這裡了,需要替換value
// 替換的時候我們可以做一些優化,因為我們第一次命中的索引出存在Entry但是Entry的key被GC了,也就是說無法被通路了,而我們這個節點是因為後移才存儲在這裡,這個時候我們這個節點是不是可以重新放回去呢?放回去後下次不是一次就命中了麼?就不需要往後周遊尋找了麼?
// 更新value
// tab[i] 與 tab[staleSlot]交換位置
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果往前探索的第一個key=null的索引下标和目前替換回去的索引相同
// 由于做了交換,我們又能保證前面不存在key == null的節點了,那麼隻需将替換後的i的值指派給slotToExpunge,這樣可以減少清理的循環次數
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 做清理工作和rehash
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
// 初始進來的時候我們有這句代碼 slotToExpunge == staleSlot
// 是以如果slotToExpunge == staleSlot仍然成立,并且目前的key == null,那麼我們就把目前的下标值指派給slotToExpunge,很好了解還是為了縮小清理的範圍,大師們對提升性能總是那麼極緻
if (k == null && slotToExpunge == staleSlot)
// 執行到了這裡,說明替換失敗了,沒找到要麼就是它的key也被GC了,要麼就是它是第一次set
// 但是目前Entry的key是null,那我們就放這裡吧,畢竟這個Entry也用不了
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// slotToExpunge != staleSlo表名需要清理key為null的Entry
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
關于replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot)往前探索并未發現滿足條件的Entry時,也就是代碼40行slotToExpunge == staleSlot滿足時,會做slotToExpunge = i操作,這個如果不清楚我做了圖來便于大家了解:
expungeStaleEntry(int staleSlot)源碼分析:
expungeStaleEntry(int staleSlot)主要做了三件事
- 從staleSlot索引開始往後周遊到第一個Entry節點不為空的下标這段區間中key=null的Entry節點清空處理
- 在周遊中如果key != null 需要做rehash處理,因為前面可能存在節點被清空了,重新根據k.threadLocalHashCode & (len - 1)計算索引,往後周遊尋找第一個為null的Entry移動到這裡
- 傳回i,這個i是從staleSlot往後周遊到的第一個為null的Entry,這個值傳回為了cleanSomeSlots(int i, int n),去清理後面的Entry,這裡你可能會疑問為啥不直接用expungeStaleEntry(int staleSlot)方法直接全部周遊一遍得了,但是你可以發現源碼這分塊的清理做了優化,具體實作請看後面的cleanSomeSlots(int i, int n)講解
private int expungeStaleEntry(int staleSlot) {
// 目前staleSlot索引處的Entry清空,注意不僅需要清空Entry還需要清空value,key本身已經為null了不需要再清空了
tab[staleSlot] = null;
size--; // 注意要及時的記錄table中Entry的個數
int i;
// 1、循環到第一個Entry不為空的位置 清空key == null的Entry和Entry的value
for (i = nextIndex(staleSlot, len);
// key == null 清空Entry和Entry的value
e.value = null;
tab[i] = null;
size--; // 注意要及時的記錄table中Entry的個數
} else {
// 2、由于做了清空處理,我們要對Entry做rehash。因為他可能可以前移
int h = k.threadLocalHashCode & (len - 1);
// 如果計算的h和目前的索引i不相等,嘗試從h開始往後尋找空的Entry
if (h != i) {
// 清空目前Entry
tab[i] = null;
// 循環找到第一個為空的Entry,并記錄它的索引
while (tab[h] != null)
h = nextIndex(h, len);
// tab[i]的值移到新的槽(可能是同一個)
tab[h] = e;
}
// 3、傳回i,這個i就是第一個為null的Entry
return i;
cleanSomeSlots(int i, int n)源碼分析:
cleanSomeSlots(int i, int n)也是對上面expungeStaleEntry(int staleSlot)方法中找到的第一個為null的Entry節點到table.legth的區間範圍内,Entry不為空但Entry的key為空的節點進行清理,這個清理不一定會進行到table的最後,因為它做了一個(n >>>= 1) != 0判斷,如果在n無符号右移1 == 0 時,并且這右移的期間沒有發現滿足清理的Entry那麼就會結束往後尋找。
n >>>=1 相當于 n= n>>>1,位運算右移一位相當于除以2
舉個例子,如果i=5,n=16,此時如果在往後周遊四次,也就是到i=9,仍然沒有滿足e != null && e.get() == null的Entry,那麼後續10-16就不再周遊了,這些都是對算法的優化。
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
do {
i = nextIndex(i, len);
Entry e = tab[i];
// 找到滿足條件的做兩個操作
// 1、重置n
// 2、調用expungeStaleEntry(i)清理
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
} while ( (n >>>= 1) != 0); // n = n >>> 1 相當于 除以2
return removed;
4.4 rehash()源碼分析
rehash()包含兩個部分的邏輯
- 從table數組的第一個節點到最後一個節點中e != null && e.get() == null的Entry執行上面的expungeStaleEntry(int staleSlot)方法
- 當達到擴容門檻值,進行擴容處理
4.4.1 rehash源碼:
private void rehash() {
// 處理table中Entry的key被GC了的元素,後面将
expungeStaleEntries();
// 這裡使用的雙倍門檻值,也就是threshold在計算了一次threshold
if (size >= threshold - threshold / 4)
resize();
4.4.2 expungeStaleEntries()源碼分析
expungeStaleEntries()源碼非常簡單,從table數組的第一個節點到最後一個節點中e != null && e.get() == null的Entry執行上面的expungeStaleEntry(int staleSlot)方法。
private void expungeStaleEntries() {
for (int j = 0; j < len; j++) {
Entry e = tab[j];
// 如果e != null && e.get() == null 即 Entry的key被GC了
// 執行expungeStaleEntry(int staleSlot)方法 -> 上面詳細分析了
if (e != null && e.get() == null)
expungeStaleEntry(j);
4.4.3 resize()源碼分析
resize()的源碼也比較簡單,主要做了三個操作:
- 執行個體化一個原先大小兩倍的數組newTab
- 周遊原先的舊數組中的每一個節點,将不為空的Entry節點計算其在新數組中的下标,放入新的數組中,放入的方式與set一緻,使用線性探測解決hash沖突,注意如果節點不為空,key為空,需要将節點和節點的value置為空,幫助GC
- 設定新的擴容門檻值,記錄新的size,替換table的引用
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
setThreshold(newLen);
size = count;
table = newTab;
4.4 getEntry(ThreadLocal<?> key)源碼分析
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
// 取出Entry
Entry e = table[i];
// 如果Entry不為空,且key相等直接傳回
if (e != null && e.get() == key)
return e; // 傳回
return getEntryAfterMiss(key, i, e); // 目前節點未命中
getEntryAfterMiss(key, i, e)源碼分析:
進入這個方法存在多種情況:
- 節點發生了hash沖突,節點插入後移了(這種情況也有可能會被GC)
- 節點為發送hash沖突,但是key被GC了
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
while (e != null) {
// key相等則直接傳回
if (k == key)
return e;
// key為空,要做清除和rehash
if (k == null)
expungeStaleEntry(i);
// 往下周遊直至末尾在從前開始 ((i + 1 < len) ? i + 1 : 0)
else
i = nextIndex(i, len);
e = tab[i];
// 可能未比對上
return null;
5、ThreadLocal記憶體洩漏
ThreadLocal記憶體洩漏是我們談及ThreadLocal存在的問題中所提及的最頻繁的一個,那麼我們接下來就從為什麼會記憶體洩漏和如何解決記憶體洩漏這兩個點來分析這個問題:
5.1 ThreadLocal為什麼會記憶體洩漏
當Thread中存在一個ThreadLocal的記憶體分布和引用情況的簡圖如下:
我們知道Entry extends WeakReference<ThreadLocal<?>>,也就是說ThreadLocal作為一個弱引用key,如果沒有被強引用所引用,那麼它将活不過下次GC,這個也是上面産生那麼多Entry的key為null的原因。當弱引用被指向的對象被GC那麼将會導緻我們程式員無法通路到這個Entry中的value對象,再加上table中的Entry它不發生hash沖突或者擴容(這些方法中都會去處理這些key為null的Entry,java大佬們一直在優化這些問題),如果線程長期存活,那麼這些key為null的Entry的value将永遠得不到GC,進而記憶體洩露。
5.2 防止記憶體洩露
防止記憶體洩露的處理方式很簡單,ThreadLocal提供了remove()方法,供程式員主動清除Entry
ThreadLocal的remove()方法:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
// 不為空則調用ThreadLocalMap的remove(ThreadLocal<?> key)方法進行清理操作
if (m != null)
m.remove(this);
ThreadLocalMap的remove(ThreadLocal<?> key)方法:
private void remove(ThreadLocal<?> key) {
// 擷取索引
// 周遊尋找目前節點
if (e.get() == key) {
// 引用置空
e.clear();
// 對其他key為null的Entry做清理和不為null的節點做rehash
clear()方法源碼:
public void clear() {
// 引用置空
this.referent = null;