1. ThreadLocal詳解
JDK1.2版本起,Java就提供了java.lang.ThreadLocal,ThreadLocal為每個使用線程都提供獨立的變量副本,可以做到線程間的資料隔離,每個線程都可以通路各自内部的副本變量。
線程上下文ThreadLocal又稱為"線程保險箱",ThreadLocal能夠将指定的變量和目前線程進行綁定,線程之間彼此隔離,持有不同的對象執行個體,進而避免了資料資源的競争。
2. ThreadLocal的使用場景
- 在進行對象跨層傳遞的時候,可以考慮ThreadLocal,避免方法多次傳遞,打破層次間的限制。
- 線程間資料隔離。
- 進行事務操作,用于儲存線程事務資訊。
注意:
ThreadLocal并不是解決多線程下共享資源的一種技術,一般情況下,每一個線程的ThreadLocal存儲的都是一個全新的對象(通過new關鍵字建立),如果多線程的ThreadLocal存儲了一個對象引用,那麼就會面臨資源競争,資料不一緻等并發問題。
3.常用方法源碼解析
3.1 initialValue方法
protected T initialValue() {
return null;
}
此方法為ThreadLocal儲存的資料類型指定的一個初始化值,在ThreadLocal中預設傳回null。但可以重寫initialValue()方法進行資料初始化。
如果使用的是Java8提供的Supplier函數接口更加簡化:
// withInitial()實際是建立了一個ThreadLocal的子類SuppliedThreadLocal,重寫initialValue()
ThreadLocal<Object> threadLocal = ThreadLocal.withInitial(Object::new);
3.2 set(T value)方法
主要存儲指定資料。
public void set(T value) {
// 擷取目前線程Thread.currentThread()
Thread t = Thread.currentThread();
// 根據目前線程擷取與之關聯的ThreadLocalMap資料結構
ThreadLocalMap map = getMap(t);
if (map != null)
// 核心方法。set 周遊整個Entry的過程,後面有詳解
map.set(this, value);
else {
// 調用createMap(),建立ThreadLocalMap,key為目前ThreadLocal執行個體,存入資料為目前value。
// ThreadLocal會建立一個預設長度為16Entry節點,并将k-v放入i位置(i位置計算方式和hashmap相似,
// 目前線程的hashCode&(entry預設長度-1)),并設定門檻值(預設為0)為Entry預設長度的2/3。
createMap(t, value);
}
}
// set 周遊整個Entry的過程
private void set(ThreadLocal<?> key, Object value) {
// 擷取所有的Entry
Entry[] tab = table;
int len = tab.length;
// 根據ThreadLocal對象,計算角标位置
int i = key.threadLocalHashCode & (len-1);
// 循環查找
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到相同的就直接覆寫,直接傳回。
if (k == key) {
e.value = value;
return;
}
// 如果ThreadLocal為null,直接驅出并使用新資料(Value)占居原來位置,
// 這個過程主要是防止記憶體洩漏。
if (k == null) {
// 驅除ThreadLocal為null的Entry,并放入Value,這也是記憶體洩漏的重點地區
replaceStaleEntry(key, value, i);
return;
}
}
// entry都為null,建立新的entry,已ThreadLocal為key,将存放資料為Value。
tab[i] = new Entry(key, value);
int sz = ++size;
// ThreadLoaclMapde的目前資料元素的個數和門檻值比較,再次進行key為null的清理工作。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 整理Entry,當Entry中的ThreadLocal對象為null時,通過重新計算角标位來清理
// 以前ThreadLocal。如果Entry數量大于3/4容量進行擴容
rehash();
}
3.3 get方法
get()用于傳回目前線程ThreadLocal中資料備份,目前線程的資料都存在一個ThreadLocalMap的資料結構中。
public T get() {
Thread t = Thread.currentThread();
// 獲得ThreadLocalMap對象map,ThreadLocalMap是和目前Thread關聯的,
ThreadLocalMap map = getMap(t);
if (map != null) {
// 存入ThreadLocal中的資料實際上是存儲在ThreadLocalMap的Entry中。
// 而此Entry是放在一個Entry數組裡面的。
// 擷取目前ThreadLocal對應的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 直接傳回目前資料
T result = (T)e.value;
return result;
}
}
// ThreadLocalMap未初始化,首先初始化
return setInitialValue();
}
// ThreadLocal的setInitialValue方法源碼
private T setInitialValue() {
// 為ThreadLocalMap指定Value的初始化值
T value = initialValue();
Thread t = Thread.currentThread();
// 根據本地線程Thread擷取ThreadLocalMap,一下方法與Set方法相同。
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果map存在,直接調用set()方法進行指派。
map.set(this, value);
else
// map==null;建立ThreadLocalMap對象,并将Thread和value關聯起來
createMap(t, value);
return value;
}
3.4 小結
- initialValue() : 初始化ThreadLocal中的value屬性值。
- set():擷取目前線程,根據目前線程從ThreadLocals中擷取ThreadLocalMap資料結構,
- 如果ThreadLocalmap的資料結構沒建立,則建立ThreadLocalMap,key為目前ThreadLocal執行個體,存入資料為目前value。ThreadLocal會建立一個預設長度為16Entry節點,并将k-v放入i位置(i位置計算方式和hashmap相似,目前線程的hashCode&(entry預設長度-1)),并設定門檻值(預設為0)為Entry預設長度的2/3。
- 如果ThreadLocalMap存在。就會周遊整個Map中的Entry節點,如果entry中的key和本線程ThreadLocal相同,将資料(value)直接覆寫,并傳回。如果ThreadLoca為null,驅除ThreadLocal為null的Entry,并放入Value,這也是記憶體洩漏的重點地區。
-
get()
get()方法比較簡單。就是根據Thread擷取ThreadLocalMap。通過ThreadLocal來獲得資料value。注意的是:如果ThreadLocalMap沒有建立,直接進入建立過程。初始化ThreadLocalMap。并直接調用和set方法一樣的方法。
3.4 ThreadLocalMap資料結構
set()還是get()方法都是避免不了和ThreadLocalMap和Entry打交道。ThreadLocalMap是一個類似于HashMap的一個資料結構(沒有連結清單),僅僅用于存放線程存放在ThreadLocal中的資料備份,ThreadLocalMap的所有方法對外部都是不可見的。
ThreadLocalMap中用于存儲資料的Entry,它是一個WeakReference類型的子類,之是以設計成WeakReference是為了能夠是JVM發生gc,能夠自動回收,防止記憶體溢出現象。
4. ThreadLocal的副作用
4.1 ThreadLocal引起髒資料
線程複用會産生髒資料。
由于結程池會重用 Thread 對象 ,那麼與 Thread 綁定的類的靜态屬性 ThreadLocal 變量也會被重用。如果在實作的線程 run()方法體中不顯式地調用 remove() 清理與線程相關的ThreadLocal 資訊,那麼如果下一個線程不調用set()設定初始值,就可能 get()到重用的線程資訊,包括 ThreadLocal 所關聯的線程對象的 value 值。
// java.lang.Thread#threadLocals
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
4.2 ThreadLocal引起的記憶體洩漏
- 在上面提到ThreadLocalMap中存放的Entry是WeakReference的子類。是以在JVM觸發GC(young gc,Full GC)時,都會導緻Entry的回收
- 在get資料的時候,增加檢查,清除已經被回收器回收的Entry(WeakReference可以自動回收)
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { ThreadLocal<?> k = e.get(); ... if (k == null) // 清除 key 是 null 的Entry expungeStaleEntry(i); ... return null; } 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]; if (e != null && e.get() == null) { n = len; removed = true; // 清除key==null 的Entry i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; } ```
- set資料時增加檢查,删除已經被垃圾回收器清理的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]; if (e != null && e.get() == null) { n = len; removed = true; // 清除key==null 的Entry i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; }
基于上面三點:ThreadLocal在一定程度上保證不會發生記憶體洩漏。但是Thread類中有ThreadlocalMap的引用,導緻對象的可達性,故不能回收。
ThreadLocal被置為null清除了。但是通過ThreadLocalMap還是被Thread類引用。導緻該資料是可達的。是以記憶體得不到釋放,除非目前線程結束,Thread引用就會被垃圾回收器回收。如圖所示
5 ThreadLocal記憶體洩漏解決方案及remove方法源碼解析
解決ThreadLocal記憶體洩漏的常用方法是:在使用完ThreadLocal之後,及時remove掉。
public void remove() {
// 根據目前線程,擷取ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// map不為null,執行remove操作
m.remove(this);
}
// ThreadLocal 的remove()
private void remove(ThreadLocal<?> key) {
// 擷取存放key-value的數組。
Entry[] tab = table;
int len = tab.length;
// 根據ThreadLocal的HashCode确定唯一的角标
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 如果和本ThreadLocal相同。将引用置null。
e.clear();
// 實行Enty和Entry.value置null。源碼中 tab[staleSlot].value = null; tab[staleSlot] = null;
expungeStaleEntry(i);
return;
}
}
}