一 前言
在多線程併發的學習中,我們總會接觸到一個類,即是ThreadLocal。這個類的作用是提供線程內的局部變量,換句話來說,就是提供一個能與當前線程綁定的局部變量。這個變量能夠在多線程併發的環境下保證每個線程中變量的獨立性。
只要線程處於活動狀態並且Threadocal實例可以訪問,每個線程就擁有對其線程局部變量副本的隱式引用;在一個線程消失之後,線程本地實例的所有副本都會被垃圾收集(除非存在對這些副本的其他引用)
我們可以看下該類的操作:
public class ThreadLocalTest {
private static ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
integerThreadLocal.set(3);
System.out.println(integerThreadLocal.get());
integerThreadLocal.remove();
System.out.println(integerThreadLocal.get());
}
}
輸出爲:
3
null
那麼效果如何呢,看下面這個例子:
public class ThreadLocalTest {
private static ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
LocalThread thread1 = new LocalThread();
thread1.setValue(3);
thread1.start();
LocalThread thread2 = new LocalThread();
thread2.setValue(4);
thread2.start();
}
public static class LocalThread extends Thread {
int value;
public void setValue(int value) {
this.value = value;
}
@Override
public void run() {
integerThreadLocal.set(value);
for (int i = 0; i < 2; i++) {
System.out.println(integerThreadLocal.get());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
輸出結果爲:
4
3
3
4
從中我們可以看出兩個線程中對於ThreadLocal類中的變量是不一樣的。
下面讓我們從源碼的角度來看一下這個類
二 源碼解析
2.1 基礎字段
先從基礎部分看起:
// hash code
private final int threadLocalHashCode = nextHashCode();
// 原子性操作類型
private static AtomicInteger nextHashCode =
new AtomicInteger();
// 每次增加的hash code
private static final int HASH_INCREMENT = 0x61c88647;
// 下一個hash值
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
從上面的定義可以知道,ThreadLocal的threadLocalHashCode
是從0開始,每新建一個ThreadLocal,對應的hashcode
就加0x61c88647。值得注意的是控制hashcode
增加的三個方法或字段爲類方法或類字段,而代表當前實例對象hashcode
的字段則是threadLocalHashCode
。
生成hash code間隙爲0x61c88647
這個魔數,可以讓生成出來的值較爲均勻地分佈在2的冪大小的數組中。對應的十進制爲1640531527。均勻分佈的好處在於很快就能探測到下一個臨近的可用slot,從而保證效率。
2.2 默認值
ThreadLocal類中也提供了默認值的設置,如下:
// 爲ThreadLocal設置默認的get初始值,需要重寫
protected T initialValue() {
return null;
}
// 創建一個線程局部變量。通過在Supplier上調用get方法確定變量的初始值
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
Supplier類是一個只有get()
方法的消費式函數式接口,通過SuppliedThreadLocal繼承了ThreadLocal類,從而實現了對於initialValue()
方法的覆蓋。
具體的使用方法如下:
// 此處輸出爲3而不是null
public static void main(String[] args) {
Supplier supplier = () -> 3;
System.out.println(ThreadLocal.withInitial(supplier).get());
}
2.3 ThreadLocalMap的定義
ThreadLocal類的內部維護了一個靜態內部類:
static class ThreadLocalMap {
}
ThreadLocalMap是一個自定義哈希映射,僅用於維護線程本地變量值。每個線程都有一個ThreadLocalMap類型的threadLocals變量。
此處爲Thread類中的字段:
ThreadLocal.ThreadLocalMap threadLocals = null;
在創建與ThreadLocal關聯的Map時,便會將當前線程與對應的Map關聯起來:
// 創建與ThreadLocal關聯的Map。在InheritableThreadLocal中重寫。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
在其內部中最核心的實現應該是Entry靜態類:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
// 將ThreadLocal註冊成爲弱引用,避免將ThreadLocal置爲null時,因爲其仍保持與Entry的關聯而導致ThreadLocal不能被垃圾收集器回收
super(k);
value = v;
}
}
Entry的key爲ThreadLocal,value爲ThreadLocal對應的值。
關於弱引用的資料可以查看:https://www.cnblogs.com/absfree/p/5555687.html
其基礎字段爲:
// 初始化容量,必須爲2的冪
private static final int INITIAL_CAPACITY = 16;
// 可增長的緩存數組
private Entry[] table;
// table中entries的數量
private int size = 0;
// 下次擴容的閾值
private int threshold; // Default to 0
// 設置調整大小閾值以保持最差的2/3負載係數。
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
// 增加i並取餘len
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
// 減少i並取餘len
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
其構造函數也可以先看下:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
// 取hash code
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 利用firstKey與firstValue構建一個新的Entry
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 設置擴容閥值,取傳入值得2/3
setThreshold(INITIAL_CAPACITY);
}
// 構造一個新映射,包括給定父映射中的所有可繼承ThreadLocals。僅由createInheritedMap調用。
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// 遍歷父映射
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
// 取出哈希槽中當前Entry的key
// 此處的e.get()方法是Reference類中的方法,返回此引用對象的引用對象
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
// key不爲空即代表對應線程的ThreadLocal未被清除
if (key != null) {
// 此處的childValue(e.vale)方法爲InheritableThreadLocal覆蓋實現
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
// 取hash code
int h = key.threadLocalHashCode & (len - 1);
// 線性探查法解決衝突
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
其他部分則留到後面講解
值得注意到的是,此處第二個構造函數的訪問限定符爲private
,那麼在ThreadLocal裏面是由哪個類來調用他的呢:
// 工廠方法創建繼承本地線程的映射。旨在僅從Thread構造函數調用。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
大家可能會問,一個Thread類擁有一個threadLocals
來存放ThreadLocalMap,那這個構建的類是來幹嘛的, 那讓我們看看到底是哪裏會調用這個方法:
答案是在Thread類中的init()
方法中會調用該方法,並將該方法的返回值放置到以下字段:
// 與此線程相關的InheritableThreadLocal值。此映射由InheritableThreadLocal類維護。
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
通過這個可以滿足開發者子線程獲得父線程ThreadLocal的需求
2.4 get()方法
// 返回當前線程副本的值,如果沒有存在則使用initialValue設置默認值並返回
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;
}
}
// 返回默認值,無則爲null
return setInitialValue();
}
此處就是之前說過的與Thread類關聯起來的字段,在LocalThread的每個實例中都可以根據這個當前線程得到相應的map:
// 獲取與ThreadLocal關聯的Map。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
並且該ThreadLocalMap類是一個靜態類,說明任何實例化ThreadLocal或其子類的對象,他們所存放的map都是同一個。
private Entry getEntry(ThreadLocal<?> key) {
// hash取址
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
// 使用線性探查法解決hash衝突
return getEntryAfterMiss(key, i, e);
}
此處的獲取get()調用了Reference類裏面的方法:
// 返回該引用對象的引用
public T get() {
return this.referent;
}
首先根據key的hash code找到在table中對應的Entry,若找到則返回e,若沒有找到則可能是因爲發生衝突而導致與當前線程關聯的entry存放在了後面,此時調用getEntryAfterMiss(key, i, e);
方法繼續尋找。此處使用了Hash表中的線性探查法。
// 在直接散列槽(direct hash slot)中找不到密鑰時使用的getEntry方法的版本。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
// 此時相當於找到當前線程對於的entry
if (k == key)
return e;
// 此處說明map的key已被置爲null,但value還在,此時需要解決清除掉已經失效的條目
if (k == null)
// 若執行該方法後,tab[i]不爲null,即是說明在i後面的數據被移動到i處,此時可以繼續尋找,否則停止尋找
expungeStaleEntry(i);
else
// 增加i並對len取餘,繼續尋找
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
從指定的位置開始不斷向下查找,如果此時找到了當前線程對應的entry則直接返回。若查找到null,在entry存在的情況下key爲null說明該數據是一個髒數據,需要清除掉已經失效的條目。若處理後該處仍爲null,則說明後續已無元素,此時便可以結束循環返回null。若處理後該處不爲null,則說明在i後面的數據被移動到i處,此時可以繼續尋找。
這裏可以清楚地瞭解到expungeStaleEntry(i)
方法是怎樣清除掉已經失效的條目:
// 通過重新處理staleSlot和下一個空槽之間的任何可能發生衝突的條目來清除陳舊條目。這也會消除在尾隨空值之前遇到的任何其他陳舊條目。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 除當前髒entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
// 持續往後查找entry[],直到遇到table[i]==null時結束,並且返回此時的i
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果在向後搜索過程中再次遇到髒entry,同樣將其清理掉
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 處理rehash的情況,相當於將Entry e重新放置到離h最近的位置,也可以說是i之前。
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// 與Knuth 6.4算法R不同,我們必須掃描到null,因爲可能有多個條目已過時。
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
該方法主要做了這麼幾件事情:
- 清理當前髒entry,即將其value引用置爲null,並且將table[staleSlot]也置爲null。
- 從當前staleSlot位置向後(nextIndex)繼續搜索,直到遇到哈希桶(tab[i])爲null的時候退出;
- 若在搜索過程再次遇到髒entry,繼續將其清除。
此處是隻針對一段哈希桶(tab[i])不爲null的片段進行操作,並沒有涉及到所有哈希槽。
若在map中沒有發現以當前線程爲key的map,則調用setInitialValue()
方法來設置初始化值:
// 用於建立initialValue,返回initialValue的值
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
根據方法名理解起來都不困難,無非是存在map則設置爲爲默認值,否則創造map
map的set方法在下面set()
方法的部分再進行講解,此時我們先來看下createMap(t, value);
:
// 創建與ThreadLocal關聯的Map。在InheritableThreadLocal中重寫。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
使用了ThreadLocalMap的構造函數。
2.5 set()方法
// 設置當前線程對應的value
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
裏面的調用方法在方法處都講過一遍了,此處簡單地過一下在get()
方法中出現過的方法調用:
getMap(t):拿取了當前線程的threadLocal變量。
createMap(t, value):調用了ThreadLocalMap的構造函數。
此處的set(ThreadLocal<?> key, Object value)
方法是在ThreadLocalMap中實現的private
方法:
// 設置與key關聯的值
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
// 緩存數組長度
int len = tab.length;
// hash code
int i = key.threadLocalHashCode & (len-1);
// 從i處開始線性探查相應的key
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到了則使新值替換舊值
if (k == key) {
e.value = value;
return;
}
// 遇到髒entry,此時通過replaceStaleEntry進行處理
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 插入新的entry
tab[i] = new Entry(key, value);
int sz = ++size;
// 若此時沒有移除任何一個髒數據且當前entry存放數量到達閥值則rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
此處我們能夠看到插入新的entry後是會對map進行擴容的,即不存在map內部全爲非null的情況,因此環形查找一定會在某一個點終止循環。
在該方法中針對髒entry做了這樣的處理:
- 如果當前table[i]!=null的話說明hash衝突就需要向後環形查找,若在查找過程中遇到髒entry就通過replaceStaleEntry進行處理;
- 如果當前table[i]==null的話說明新的entry可以直接插入,但是插入後會調用cleanSomeSlots方法檢測並清除髒entry
當遇到髒數據的時候使用replaceStaleEntry(key, value, i)
方法清除髒數據,並且將指定值插入到指定鍵的位置:
// 將設置操作期間遇到的髒entry替換爲指定鍵的entry。無論是否已存在指定鍵的entry,傳入value參數的值將被存儲在entry中。
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 向前找到第一個髒entry
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
// 往後環形查找
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果在向後環形查找過程中發現key相同的entry就覆蓋並且和髒entry進行交換
if (k == key) {
// 修改值
e.value = value;
// 將該entry與前方置空的hash Slot互換
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果向前未搜索到髒entry,並且在查找過程中還未發現髒entry,那麼就以當前位置作爲cleanSomeSlots的起點
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 搜索髒entry並進行清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//如果向前未搜索到髒entry,則在查找過程遇到髒entry的話,後面就以此時這個位置作爲起點執行cleanSomeSlots
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果在查找過程中沒有找到可以覆蓋的entry,則將新的entry插入在髒entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
由於該方法中存在向前環形查找髒entry與向後查找可替換entry,因此可以分爲四種情況來進行講解:
-
向前查找到了髒entry
-
向後查找到了可覆蓋的entry
一開始slotToExpunge初始狀態與staleSlot,當向前環形查找到了髒entry時,則將slotToExpunge更新爲當前髒數據的索引,直到遇到
table[i] == null
時前向搜索結束。接下來的for循環中進行了後向環形查找,如果找到了可覆蓋的entry,則先覆蓋當前位置i
中的entry,然後再與staleSlot位置上的髒entry進行交換。e.value = value; // 將該entry與前方置空的hash Slot互換 tab[i] = tab[staleSlot]; tab[staleSlot] = e;
交換後
i
處的位置就是髒數據了。最後使用cleanSomeSlots
方法以slotToExpunge
爲起點開始清理髒entry。此處之所有不用
slotToExpunge
與可覆蓋entry進行交換的原因是:該map中的鍵值對是進行環形查找的,因此slotToExpunge
不一定會在指定key的hash的後面,而staleSlot
因爲一開始就是因爲插入時候才被發現是髒數據,因此直接與其交換是沒有問題的。 -
向後沒有查找到可覆蓋的entry
一開始slotToExpunge初始狀態與staleSlot,當向前環形查找到了髒entry時,則將slotToExpunge更新爲當前髒數據的索引,直到遇到
table[i] == null
時前向搜索結束。接下來的for循環中進行了後向環形查找,如果沒有找到可覆蓋的entry並且後向環形查找過程結束。則將新插入的entry直接放在staleSlot處。tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value);
最後使用
cleanSomeSlots
方法以slotToExpunge
爲起點開始清理髒entry。
-
-
向前環形查找沒有查找到髒entry
-
向後查找到了可覆蓋的entry
若在前向環形查找的整個過程中都沒有遇到髒entry,則slotToExpunge的初始狀態依舊與staleSlot相同。接下來的for循環進行環形查找,若此時遇到了髒entry,則更新slotToExpunge的位置爲當前位置i。
if (k == null && slotToExpunge == staleSlot) slotToExpunge = i;
若查找到了可覆蓋的entry,則先覆蓋當前位置的entry,然後再與staleSlot位置上的髒entry進行交換。
e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e;
最後使用
cleanSomeSlots
方法以slotToExpunge
爲起點開始清理髒entry。 -
向後環形查找沒有可覆蓋的entry
若在前向環形查找的整個過程中都沒有遇到髒entry,則slotToExpunge的初始狀態依舊與staleSlot相同。接下來的for循環進行環形查找,若此時遇到了髒entry,則更新slotToExpunge的位置爲當前位置i。 並且將slotToExpunge更新到當前位置。
if (slotToExpunge == staleSlot) slotToExpunge = i;
最後使用
cleanSomeSlots
方法以slotToExpunge
爲起點開始清理髒entry。
-
以上便是replaceStaleEntry
方法中的所有情況。下面我們再看一下是怎樣使用cleanSomeSlots
方法來清理髒entry的:
首先是一開始調用的expungeStaleEntry(slotToExpunge)
方法,會清除指定staleSlot
後面table[i]不爲null範圍內的一系列髒entry。最後返回的是清除髒entry後遇到的第一個entry爲null的索引,具體可以看上文對其的分析。
// 嘗試掃描並處理一些髒entry。當添加新元素或另一個髒entry被清除時,將調用此方法。
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
// 清除髒entry
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
在調用該方法的情況中,索引i所指向的entry可能是一個空entry或者剛剛放入一個新entry,反正肯定不是一個髒entry。
參數n的用處主要用於掃描控制(scan control),從while中是通過n來進行條件判斷的說明n就是用來控制掃描趟數(循環次數)的。在掃描過程中,如果沒有遇到髒entry就整個掃描過程持續log2(n)次,log2(n)的得來是因爲n >>>= 1
,每次n右移一位相當於n除以2。如果在掃描過程中遇到髒entry的話就會令n爲當前hash表的長度(n=len
),再掃描log2(n)趟,注意此時n增加無非就是多增加了循環次數從而通過nextIndex往後搜索的範圍擴大。
不是很清楚爲什麼使用n >>>= 1
來判斷掃描趟數,如果有懂的大神歡迎留言。。
如果是在set方法插入新的entry後調用,n位當前已經插入的entry個數size;如果是在replaceSateleEntry方法中調用,n爲哈希表的長度len。
如果有清理過髒entry則返回true,否則返回false。
最後的最後,如果將創建了一個新的entry並且此時不存在髒數據,且map中存儲的entry數量已經達到閥值時,便需要rehash
了。
private void rehash() {
expungeStaleEntries();
// 使用較低的閾值加倍以避免滯後
if (size >= threshold - threshold / 4)
resize();
}
首先是清除髒entry:
// 清除緩存數組中的所有髒entry
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
前面是判斷size >= 2 * threshold / 3
,此處則判斷了size >= 3 * threshold / 4
。符合條件則調用resize()
:
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 擴容爲兩倍
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
// 將舊map中的entry遷移到新entry上
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
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;
}
2.6 remove()方法
在上述的方法中,我們經常能夠看到在索引位置i中,Entry e = table[i],e != null && e.get() == 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)]) {
// 找到包含指定key的entry
if (e.get() == key) {
// 清除,即將key置爲null,此時該entry變成了髒數據
e.clear();
// 清除髒數據
expungeStaleEntry(i);
return;
}
}
}
public void clear() {
this.referent = null;
}
this.referent = null;
是由於Entry繼承了WeakPerference,使用了referent
來存儲key
,將其置爲null
便等同於將key
置爲null
。
三 結論
與我們常用的Map不同,java裏大部分Map都是用鏈表發解決hash衝突的,而 ThreadLocalMap 採用的是開放定址法。使用均勻的hash算法能夠很好地配合開放定址法的工作,避免產生過多的衝突。而涉及到開放定址法肯定會有過期值清理的問題,在ThreadLocalMap中使用了expungeStaleEntry
、replaceStaleEntry
、cleanSomeSlots
三個方法來解決過期值的問題,防止內存泄漏。相較於拉鍊法來說使用開放定址法可以節省一些指針空間。
看到這裏可能有些同學會發現一個問題,get()、set()、remove()方法都對髒數據進行了處理,那麼這個髒數據是怎麼出現的呢?這個問題就涉及到了弱引用。一個ThreadLocal一般會有兩處地方對其進行引用,一個是線程在操作期間對其的強引用,另外一個是ThreadLocalMap對其的弱引用。但是在ThreadLocal中將ThreadLocalMap對其的引用設置爲弱引用,因此當線程在操作期間將ThreadLocal的實例強引用設爲null,該實例所對應的內存就會被回收,因此entry就存在key爲null的情況,無法通過一個Key爲null去訪問到該entry的value。
key是被回收了,但是value呢?由於此時通過這條引用鏈:當前線程–>threadLocalMap(線程中的threadLocals
)–>entry–>value引用–>value內存。這時候的value無法被GC回收,這樣就造成了嚴重的內存泄漏。因此我們需要在操作get()、set()、remove()方法時都對髒數據進行了處理。
從這裏我們可以看出,threadLocal有可能存在內存泄漏,顯式地進行remove是個很好的編碼習慣,這樣是不會引起內存泄漏。
下面我們再看下threadLocalMap
的生命週期:
下面是Thread類中的exit()
方法,當線程退出時會執行該方法:
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
// 這裏兩段釋放了對threadLocalMap的引用
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
從中我們可以看出,當所有使用了ThreadLocal的線程退出時,ThreadLocalMap的實例便會被GC回收。