本文原創地址,
我的博客
:https://jsbintask.cn/2019/04/01/jdk/jdk8-threadlocal/(食用效果最佳),轉載請註明出處!
前言
ThreadLocal
是jdk中一個非常重要的工具,它可以控制堆內存中的對象只能被指定線程訪問,如果你經常閱讀源碼,基本在各大框架都能發現它的蹤影。而它最經典的應用就是事務管理
,同時它也是面試中的常客。
原理
我們知道,堆內存是共享的,爲什麼ThreadLocal能夠控制指定線程訪問呢? 如圖:
- 調用ThreadLocal的
get
方法。 - 獲取當前線程t1.
- 獲取t1的成員變量
ThreadLocalMap
。 - 根據ThreadLocal的hashcode計算出ThreadLocalMap中Entry[]數組的索引。
- 返回索引位置的值。
這樣我們就很容易理解了,爲什麼只有當前線程才能獲取到某些值,因爲這是這些值都直接保存在當前線程的成員變量ThreadLocalMap中,而ThreadLocal在這個過程中充當的角色則是提供它獨一無二的hashcode值,這樣我們就能計算出我們保存的值在ThreadLocalMap的位置。
源碼分析
我們從構建一個ThreadLocal到調用它的set,get方法完整的分析一遍它的源碼。
構造器
當我們使用new ThreadLocal<>()
new一個ThreadLocal對象時,它初始化了一個成員變量threadLocalHashCode
,這個成員變量代表當前ThreadLocal的hashcode值,而它肯定是唯一的:
- ThreadLocal內部有一個靜態hashCode生成器
nextHashCode
。 - 每次新new一個ThreadLocal對象,調用這個生成器同步方法獲取hashcode。
因爲依賴於靜態成員變量nextHashCode
的關係,所以它的hashcode肯定唯一!
set(T t)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
- 獲取當前線程t。
- 從t中獲取
ThreadLocalMap
map。
- 如果map不爲空,將當前值value放入map。
- 如果map爲空,新建一個ThreadLocalMap放入線程t。
ThreadLocalMap是ThreadLocal中的內部類,它的結構如下:
public class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
private int size = 0;
private static final int INITIAL_CAPACITY = 16;
private int threshold; // Default to 0
}
類似於ArrayList內部的構造,它內部有一個Entry
數組table,並且Entry繼承自弱引用,所以每一個Entry中保存着兩個值,ThreadLocal
,value
,value即是我們要保存的值。這裏值得注意的是,它保存的ThreadLocal爲弱引用(gc時會被標記清理).
接着,我們回過頭詳細分析第三步,ThreadLocalMap的set方法:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 1
for (Entry e = tab[i];
e != null; // 2
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) { // 3
e.value = value;
return;
}
if (k == null) { // 4
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value); // 5
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 6
rehash();
}
- 根據ThreadLocal的hashCode計算出在entry中的索引i。
- 取出i對應的Entry值e。
- 如果e的key等於當前ThreadLocal,代表已經有一個一樣的ThreadLocal在這個entry設值,直接替換這個entry上的value。
- e上面的ThreadLocal爲null,代表垃圾收集器準備回收這個Entry了,我們需要放入一個新的Entry。重新計算數組大小,重新hash。
- i位置還沒有初始化(第一次set這個ThreadLocal),直接將value放到i的位置。
- 擴容Entry數組。
get()
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;
}
}
return setInitialValue();
}
- 獲取當前線程。
- 從當前線程中獲取ThreadLocalMap
- 從ThreadLocalMap中找出ThreadLocal對應的Entry.
- 如果Entry不爲null,直接返回Entry中的value
- 返回初始值。
其中,ThreadLocalMap的get(ThreadLocal tl)如下:
它和我們一開始的分析一樣,根據ThreadLocal的hashcode成員變量計算出索引位置i,得到Entry。這裏同樣有特殊情況,如果得到的Entry的key和當前ThreadLocal不相等,代表這個Entry將被垃圾收集處理,調用getEntryAfterMiss
rehash,計算數組大小。
注意事項
從上面的代碼分析中,我們知道,ThreadLocalMap的生命週期和當前線程同步,如果當前線程被銷燬,則map中的所有引用均被銷燬。但如果當前線程不被銷燬呢(線程池,tomcat處理請求等)?Entry中保存了ThreadLocal的弱引用以及value,gc時可能清理掉ThreadLocal,而這個value確再沒有訪問之地,這個時候就會造成內存泄漏!
所以我們需要手動調用remove方法清理掉當前線程ThreadLocalMap的引用!
總結
- ThreadLocal中真正保存的值還是在線程的ThreadLocalMap中,ThreadLocal只是使用它的hashcode值充當中間計算變量。
- ThreadLocalMap內部使用一個Entry數組保存數據。
- ThreadLocal可能出現內存泄漏的情況,最好手動調用remove方法。
關注我,這裏只有乾貨!