Java併發程式設計之ThreadLocal詳解
ThreadLocal是什麼?
ThreadLocal是一個關於建立執行緒區域性變數的類。
通常情況下,我們建立的變數是可以被任何一個執行緒訪問並修改的。而使用ThreadLocal建立的變數只能被當前執行緒訪問,其他執行緒則無法訪問和修改。
ThreadLocal使用示例
示例1:ThreadLocal宣告基本型別變數
執行程式,可以得到:
從執行結果可以看出,對於基本型別變數,ThreadLocal確實是可以達到執行緒隔離作用的。
示例2:ThreadLocal宣告自定義型別的物件
執行程式,可以得到:
從執行結果可以看出,對於自定義型別的物件,ThreadLocal也是可以達到執行緒隔離作用的。
示例3:ThreadLocal宣告的變數都指向同一個物件
對示例2的程式碼稍作修改,使得ThreadLocal宣告的變數初始化時不再例項化一個新的物件,而是讓它指向同一個物件,執行檢視結果:
很顯然,在這裡,並沒有通過ThreadLocal達到執行緒隔離的機制,可是ThreadLocal不是保證執行緒安全的麼?這是什麼鬼? 顯然,雖說ThreadLocal讓訪問某個變數的執行緒都擁有自己的區域性變數,但是如果這個區域性變數都指向同一個物件的話,這個時候,ThreadLocal就失效了。
ThreadLocal原始碼剖析
ThreadLocal類的原始碼在java.lang包中。其中主要有四個方法:
1. get()
// 返回當前執行緒所對應的執行緒變數 public T get() { // 獲取當前執行緒 Thread t = Thread.currentThread(); // 獲取當前執行緒的成員變數 threadLocal 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(); }
從原始碼中可以看到,get()方法首先通過當前執行緒獲取所對應的成員變數ThreadLocalMap,然後通過ThreadLocalMap獲取當前ThreadLocal的鍵值對Entry,最後通過該Entry獲取目標值result。
其中,getMap()方法可以獲取當前執行緒所對應的ThreadLocalMap,其原始碼如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
2. set(T 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);
}
set方法首先獲取當前執行緒所對應的ThreadLocalMap,如果不為空,則呼叫ThreadLocalMap的set()方法,key就是當前ThreadLocal,如果不存在,則呼叫createMap()方法新建一個,其原始碼如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3. initialValue()
// 返回該執行緒區域性變數的初始值。
protected T initialValue() {
return null;
}
該方法定義為protected級別且返回為null,很明顯是要子類重寫來實現它的,所以我們在使用ThreadLocal的時候一般都應該覆蓋該方法。該方法不能顯示呼叫,只有在第一次呼叫get()或者set()方法時才會被執行,並且僅執行1次。
4. remove()
// 將當前執行緒區域性變數的值刪除
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
該方法的目的是減少記憶體佔用,避免出現因為執行緒遲遲未結束而導致記憶體洩漏的情況。需要指出的是,當執行緒結束後,對應該執行緒的區域性變數將自動被垃圾回收,所以顯式呼叫該方法清除執行緒的區域性變數並不是必須的操作,但它可以加快記憶體回收的速度。
ThreadLocalMap類
從ThreadLocal的原始碼中我們可以看到,ThreadLocal的實現比較簡單,主要是依賴於ThreadLocalMap這個類,我們有必要好好理解一下後者。
根據命名就可以看出,ThreadLocalMap,它實際上是一個Map鍵值對。在其內部使用了Entry的方式來實現key-value的儲存:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在上面的程式碼中,Entry內的Key就是ThreadLocal,而Value就是執行緒私有的那個變數。同時,Entry也繼承WeakReference,所以說Entry所對應key(ThreadLocal例項)的引用是一個弱引用。
下面來看一下ThreadLocalMap類中幾個核心的方法:
1. set()
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
原始碼的意思簡單明瞭,根據要儲存的key到Entry陣列中去匹配,如果key已經存在就更新值,否則建立新的entry寫入。
值得注意的是,這裡的set()操作和我們在集合Map瞭解的put()方式有點兒不一樣,雖然他們都是key-value結構,不同點在於他們解決雜湊衝突的方式不同。 集合Map的put()採用的是拉鍊法,即在每個陣列元素的位置,存入連結串列來解決衝突。而ThreadLocalMap的set()則是採用開放定址法來解決衝突的。
set()操作除了儲存元素外,還有一個很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),這兩個方法可以清除掉key == null 的例項,防止記憶體洩漏。在set()方法中還有一個變數很重要:threadLocalHashCode,定義如下:
private final int threadLocalHashCode = nextHashCode();
從名字上面我們可以看出threadLocalHashCode應該是ThreadLocal的雜湊值,定義為final,表示ThreadLocal一旦建立其雜湊值就已經確定了,生成過程則是呼叫nextHashCode():
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
nextHashCode表示分配下一個ThreadLocal例項的threadLocalHashCode的值,HASH_INCREMENT則表示分配兩個ThradLocal例項的threadLocalHashCode的增量,從nextHashCode就可以看出他們的定義。
2. getEntry()
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
由於採用了開放定址法,所以當前key的雜湊值和元素在陣列的索引並不是完全對應的,首先取一個探測數(key的雜湊值),如果所對應的key就是我們要找的元素,則返回,否則呼叫getEntryAfterMiss()再尋找,原始碼如下:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
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時,呼叫了expungeStaleEntry()方法,該方法用於處理key == null,有利於GC回收,能夠有效地避免記憶體洩漏。
ThreadLocal與記憶體洩漏
圖:ThreadLocal實現原理
前面提到過,每個Thread都有一個ThreadLocal.ThreadLocalMap,該map的key為ThreadLocal例項的一個弱引用,我們知道弱引用有利於GC回收。當ThreadLocal的key == null時,GC就會回收這部分空間,但是value卻不一定能夠被回收。
如果當前執行緒再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收,造成記憶體洩漏。
其實,ThreadLocal類的設計中已經考慮到這種情況,也加上了一些防護措施:在觸發ThreadLocal的remove()時會清除執行緒ThreadLocalMap裡key為null的value。