ThreadLocal原始碼解析
在多執行緒的情況下,ThreadLocal提供了一個種為每個執行緒訪問相同的變數,並且執行緒對變數的更新互不影響的機制。也是物件實現執行緒安全的一種方式。
ThreadLocal的實現機制
我們常用的方法有get
、set
和initialValue
,這次將會圍繞這幾個方法的原始碼進行深入解析
- get方法
// 獲取元素 public T get() { // 當前執行緒 Thread t = Thread.currentThread(); // 通過當前執行緒獲取ThreadLocalMap物件 ThreadLocalMap map = getMap(t); if (map != null) { // 獲取Entry,其中key為ThreadLocal物件自身 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; // 獲取物件的值 return result; } } // 返回initialValue的值 return setInitialValue(); }
首先,通過當前執行緒物件獲取ThreadLocalMap
物件,然後以ThreadLocal
物件自身為key獲取ThreadLocalMap.Entry
,最後在獲取Entry
中的value
程式碼的邏輯非常簡單,我們再來看看getMap
和map.getEntry
方法
- getMap方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
threadLocals
是Thread的一個屬性
public class Thread implements Runnable { // ...... // threadLocals是Thread的一個屬性 ThreadLocal.ThreadLocalMap threadLocals = null; }
- map.getEntry方法
ThreadLocalMap
是ThreadLocal
物件的一個內部類,Entry
是ThreadLocalMap
的一個內部類
// ThreadLocal的內部類 static class ThreadLocalMap { // Entry是ThreadLocalMap的內部類,是一個弱引用物件 static class Entry extends WeakReference<ThreadLocal<?>> { // ThreadLocal中的value Object value; // Entry的Key為ThreadLocal物件 Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // Entry陣列,用來存放一個執行緒的多個ThreadLocal變數 private Entry[] table; // 根據ThreadLocal來獲取對應的value private Entry getEntry(ThreadLocal<?> key) { // 通過hash演算法獲取key在陣列中對應的下標 int i = key.threadLocalHashCode & (table.length - 1); // 獲取下標對應的Entry物件 Entry e = table[i]; // 獲取value if (e != null && e.get() == key) return e; else // 當key不存在時獲取值,有2中可能 // 1. 可能過期了 // 2. 可能擴縮容 return getEntryAfterMiss(key, i, e); } }
當key
過期瞭如何獲取值
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
// 如果key存在,直接返回value
if (k == key)
return e;
// 如果key為空說明已經過期了,需要清除
if (k == null)
expungeStaleEntry(i);
else
// 獲取下一個key,看看能否找到
// 這是由於清除已經過期的key,
// 改變了Entry陣列的size引起的位置變更
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
- 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);
}
先獲取ThreadLocalMap
物件,然後在以ThreadLocal
為key
,將value
設定到ThreadLocalMap
物件中
- map.set方法
// 將ThreadLocal對應的value儲存到ThreadLocalMap物件中
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 計算table中的下標
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 獲取ThreadLocal物件
ThreadLocal<?> k = e.get();
// 如果Entry陣列中存在ThreadLocal物件,則替換之前的值
if (k == key) {
e.value = value;
return;
}
// 去掉過期的ThreadLocal物件
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// Entry陣列中不存在ThreadLocal物件,建立一個新的Entry物件
tab[i] = new Entry(key, value);
int sz = ++size;
// 清除過期的物件
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// Entry陣列的大小改變以後重新計算hash
rehash();
}
- createMap方法
void createMap(Thread t, T firstValue) {
// 當執行緒的threadLocals為null時,為執行緒初始化一個ThreadLocalMap物件
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- initialValue方法
// 可以通過重寫該方法來返回預設值
protected T initialValue() {
return null;
}
ThreadLocal記憶體洩漏問題
首先看一下ThreadLocal中物件的引用關係圖
從ThreadLocal中物件的引用關係來看,Thread
、ThreadLocalMap
、Entry
物件之間都是強引用,如果可能出現記憶體洩漏那就是ThreadLocal
物件弱引用引起的。
什麼時候會發生記憶體洩漏
當ThreadLocal
例項不在有強引用指向,只有弱引用存在,且GC回收了這部分空間時,也就是Entry
物件中的key
被回收了,但是value還沒有被回收,這時會出現記憶體洩漏,因為value
無法得到釋放。
如何避免記憶體洩漏
ThreadLocalMap
中是通過expungeStaleEntry
將key
為null
的物件對應的value
也設定為null
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果key為null,會將value也設定成null
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
所以只要呼叫expungeStaleEntry
方法,且key
為null
時就可以回收掉value
了,我們可以通過呼叫ThreadLocal
的remove
方法進行釋放
避免ThreadLocal出現記憶體洩漏的方式有:
- 呼叫
ThreadLocal
的remove
方法 - 將
ThreadLocal
變數定義成static
型別的,對ThreadLocal
的強引用不會消失,所以也不存在記憶體洩漏的問題,但是可能會有所浪費