ThreadLocal
1. ThreadLoacal 是什麼
該類提供了線程局部 (thread-local
) 變量。這些變量不同於它們的普通對應物,因爲訪問某個變量(通過其get
或 set
方法)的每個線程都有自己的局部變量,它獨立於變量的初始化副本。
ThreadLocal
實例通常是類中的 private static
字段,它們希望將狀態與某一個線程(例如,用戶 ID
或事務 ID
)相關聯。
所以ThreadLocal
與線程同步機制不同,線程同步機制是多個線程共享同一個變量,而ThreadLocal
是爲每一個線程創建一個單獨的變量副本,故而每個線程都可以獨立地改變自己所擁有的變量副本,而不會影響其他線程所對應的副本。可以說ThreadLocal
爲多線程環境下變量問題提供了另外一種解決思路。
ThreadLocal
定義了四個方法:
get()
:返回此線程局部變量的當前線程副本中的值。initialValue()
:返回此線程局部變量的當前線程的“初始值”。remove()
:移除此線程局部變量當前線程的值。set(T value)
:將此線程局部變量的當前線程副本中的值設置爲指定值。
除了這四個方法,ThreadLocal
內部還有一個靜態內部類ThreadLocalMap
,該內部類纔是實現線程隔離機制的關鍵,get()
、set()
、remove()
都是基於該內部類操作。ThreadLocalMap
提供了一種用鍵值對方式存儲每一個線程的變量副本的方法,key
爲當前ThreadLocal
對象,value
則是對應線程的變量副本。
對於ThreadLocal
需要注意的有兩點:
1、ThreadLocal
實例本身是不存儲值,它只是提供了一個在當前線程中找到副本值的key
。
2、是ThreadLocal
包含在Thread中,而不是Thread
包含在ThreadLocal中。
下圖是Thread
、ThreadLocal
、ThreadLocalMap
的關係:
2. 使用示例
public class SeqCount {
private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
// 實現initialValue()
public Integer initialValue() {
return 0;
}
};
public int nextSeq(){
seqCount.set(seqCount.get() + 1);
return seqCount.get();
}
public static void main(String[] args){
SeqCount seqCount = new SeqCount();
SeqThread thread1 = new SeqThread(seqCount);
SeqThread thread2 = new SeqThread(seqCount);
SeqThread thread3 = new SeqThread(seqCount);
SeqThread thread4 = new SeqThread(seqCount);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
private static class SeqThread extends Thread{
private SeqCount seqCount;
SeqThread(SeqCount seqCount){
this.seqCount = seqCount;
}
public void run() {
for(int i = 0 ; i < 3 ; i++){
System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
}
}
}
}
3. ThreadLocal源碼解析
ThreadLocal
雖然解決了這個多線程變量的複雜問題,但是它的源碼實現卻是比較簡單的。ThreadLocalMap
是實現ThreadLocal
的關鍵,我們先從它入手。
3.1 ThreadLocalMap
ThreadLocalMap
其內部利用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
的源碼稍微多了點,我們就看兩個最核心的方法getEntry()
、set(ThreadLocal key, Object value)
方法。
3.1.1 set(ThreadLocal key, Object value)
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// 根據 ThreadLocal 的散列值,查找對應元素在數組中的位置
int i = key.threadLocalHashCode & (len-1);
// 採用“線性探測法”,尋找合適位置
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key 存在,直接覆蓋
if (k == key) {
e.value = value;
return;
}
// key == null,但是存在值(因爲此處的e != null),說明之前的ThreadLocal對象已經被回收了
if (k == null) {
// 用新元素替換陳舊的元素
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal對應的key實例不存在也沒有陳舊元素,new 一個
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
// cleanSomeSlots 清楚陳舊的Entry(key == null)
// 如果沒有清理陳舊的 Entry 並且數組中的元素大於了閾值,則進行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
這個set()
操作和我們在集合瞭解的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
就可以看出他們的定義。
3.1.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
回收,能夠有效地避免內存泄漏。
3.2 ThreadLocal
3.2.1 get()
返回當前線程所對應的線程變量。
public T get() {
// 獲取當前線程
Thread t = Thread.currentThread();
// 獲取當前線程的成員變量 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 從當前線程的ThreadLocalMap獲取相對應的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 獲取目標值
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
首先通過當前線程獲取所對應的成員變量ThreadLocalMap
,然後通過ThreadLocalMap
獲取當前ThreadLocal
的Entry
,最後通過所獲取的Entry
獲取目標值result
。
3.2.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);
}
獲取當前線程所對應的ThreadLocalMap
,如果不爲空,則調用ThreadLocalMap
的set()
方法,key
就是當前ThreadLocal
,如果不存在,則調用createMap()
方法新建一個,如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
3.2.3 initialValue()
返回該線程局部變量的初始值。
protected T initialValue() {
return null;
}
該方法定義爲protected
級別且返回爲null
,很明顯是要子類實現它的,所以我們在使用ThreadLocal
的時候一般都應該覆蓋該方法。該方法不能顯示調用,只有在第一次調用get()
或者set()
方法時纔會被執行,並且僅執行1
次。
3.2.4 remove()
將當前線程局部變量的值刪除。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
該方法的目的是減少內存的佔用。當然,我們不需要顯示調用該方法,因爲一個線程結束後,它所對應的局部變量就會被垃圾回收。
4. ThreadLocal 爲什麼會內存泄漏
前面提到每個Thread
都有一個ThreadLocal.ThreadLocalMap
的map
,該map
的key
爲ThreadLocal
實例,它爲一個弱引用,我們知道弱引用有利於GC
回收。當ThreadLocal的key=null
時,GC
就會回收這部分空間,但是value
卻不一定能夠被回收,因爲他還與Current Thread
存在一個強引用關係,如下:
由於存在這個強引用關係,會導致value
無法回收。如果這個線程對象不會銷燬那麼這個強引用關係則會一直存在,就會出現內存泄漏情況。所以說只要這個線程對象能夠及時被GC
回收,就不會出現內存泄漏。如果碰到線程池,那就更坑了。
那麼要怎麼避免這個問題呢?
在前面提過,在ThreadLocalMap
中的setEntry()
、getEntry()
,如果遇到key = null
的情況,會對value
設置爲null。當然我們也可以顯示調用ThreadLocal
的remove()
方法進行處理。