Java讀原始碼之ThreadLocal
前言
JDK版本: 1.8
之前在看Thread原始碼時候看到這麼一個屬性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal實現的是每個執行緒都有一個本地的副本,相當於區域性變數,其實ThreadLocal就是內部自己實現了一個map資料結構。
ThreadLocal確實很重要,但想到看原始碼還是有個小故事的,之前去美團點評面試,問我如何儲存使用者登入token,可以避免層層傳遞token?
心想這好像是在說ThreadLocal,然後開始胡說放在redis裡或者搞個ThreadLocal,給自己挖坑了
面試官繼續問,ThreadLocal使用時候主要存在什麼問題麼?
完蛋,確實只瞭解過,沒怎麼用過,涼涼,回來查了下主要存在的問題如下
- ThreadLocal可能記憶體洩露?
帶著疑惑進入原始碼吧
原始碼
類宣告和重要屬性
package java.lang; public class ThreadLocal<T> { // hash值,類似於Hashmap,用於計算放在map內部陣列的哪個index上 private final int threadLocalHashCode = nextHashCode(); private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);} // 初始0 private static AtomicInteger nextHashCode = new AtomicInteger(); // 神奇的值,這個hash值的倍數去計算index,分佈會很均勻,總之很6 private static final int HASH_INCREMENT = 0x61c88647; static class ThreadLocalMap { // 注意這是一個弱引用 static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // 初始容量16,一定要是2的倍數 private static final int INITIAL_CAPACITY = 16; // map內部陣列 private Entry[] table; // 當前儲存的數量 private int size = 0; // 擴容指標,計算公式 threshold = 總容量 * 2 / 3,預設初始化之後為10 private int threshold;
增改操作
讓我們先來看看增改方法
public void set(T value) { Thread t = Thread.currentThread(); // 拿到當前Thread物件中的threadLocals引用,預設threadLocals值是null ThreadLocalMap map = getMap(t); if (map != null) // 如果ThreadLocalMap已經初始化過,就把當前ThreadLocal例項的引用當key,設定值 map.set(this, value); //下文詳解 else // 如果不存在就建立一個ThreadLocalMap並且提供初始值 createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
讓我們來看看map.set(this, value)具體怎麼操作ThreadLocalMap
private void set(ThreadLocal<?> key, Object value) {
// 獲取ThreadLocalMap內部陣列
Entry[] tab = table;
int len = tab.length;
// 算出需要放在哪個桶裡
int i = key.threadLocalHashCode & (len-1);
// 如果當前桶衝突了,這裡沒有用拉鍊法,而是使用開放定指法,index遞增直到找到空桶,資料量很小的情況這樣效率高
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 拿到目前桶中key
ThreadLocal<?> k = e.get();
// 如果桶中key和我們要set的key一樣,直接更新值就ok了
if (k == key) {
e.value = value;
return;
}
// 桶中key是null,因為是弱引用,可能被回收掉了,這個時候我們直接佔為己有,並且進行cleanSomeSlots,當前key附近區域性清理其他key是空的桶
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 如果沒衝突直接新建
tab[i] = new Entry(key, value);
int sz = ++size;
// 當前key附近區域性清理key是空的桶,如果一個也沒清除並且當前容量超過閾值了就擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void rehash() {
// 這個方法會清除所有key為null的桶,清理完後size的大小會變小
expungeStaleEntries();
// 此時size還大於閾值的3/4就擴容
if (size >= threshold - threshold / 4)
// 2倍擴容
resize();
}
為什麼會記憶體洩漏
總算讀玩了set,大概明白了為什麼會發生記憶體洩漏,畫了個圖
ThreadLocalMap.Entry中的key儲存了ThreadLocal例項的一個弱引用,如果ThreadLocal例項棧上的引用斷了,只要GC一發生,就鐵定被回收了,此時Entry的key,就是null,但是呢Entry的value是強引用而且是和Thread例項生命週期繫結的,也就是執行緒沒結束,值就一直不會被回收,所以產生了記憶體洩漏。
總算明白了,為什麼一個set操作要這麼多次清理key為null的桶。
既然這麼麻煩,為什麼key一定要用弱引用?
繼續看上面的圖,如果我們的Entry中儲存的是ThreadLocal例項的一個強引用,我們刪掉了ThreadLocal棧上的引用,同理此時不僅value就連key也不會回收了,這記憶體洩漏就更大了
查詢操作
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;
}
}
// 返回null
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果只是threadLocals.Entry是空,就設定value為null
map.set(this, value);
else
// 如果threadLocals是空,就new 一個key是當前ThreadLocal,value是空的ThreadLocalMap
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
讓我們來看看map.getEntry(this)具體怎麼操作ThreadLocalMap
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
// 最好情況,定位到了Entry,並且key匹配
return e;
else
// 可能是hash衝突重定址了,也可能是key被回收了
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 向後遍歷去匹配key,同時清除key為null的桶
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
如何避免記憶體洩漏
新增,查詢中無處不在的去清理key為null的Entry,是不是我們就可以放心了,大多數情況是的,但是如果我們在使用執行緒池,核心工作執行緒是不會停止的,會重複利用,這時我們的Entry中的value就永遠不會被回收了這很糟糕,還好原始碼作者還沒給我提供了remove方法,綜上所述,養成良好習慣,只要使用完ThreadLocal,一定要進行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)]) {
if (e.get() == key) {
// 主要多了這一步,讓this.referent = null,GC會提供特殊處理
e.clear();
expungeStaleEntry(i);
return;
}
}
}