一、基本概念
1.1 ThreadLocal 的用途
首先饵撑,我們來看一下JDK
源碼中對于ThreadLocal
的解釋:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one has its own, independently initialized copy of the variable. ThreadLocal instances are typically privatestatic fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
翻譯過來就是:
ThreadLocal
用來提供線程內(nèi)的局部變量开仰。這些變量在多線程環(huán)境下訪問時能夠保證各個線程里的變量相對獨立于其它線程內(nèi)的變量,ThreadLocal
實例通常來說都是private static
類型的瞧柔。
因此语稠,ThreadLocal
適用于滿足下面條件的場景:
- 每個線程 有且僅有 該對象的一個實例
- 在該線程的整個生命周期內(nèi) 有多處用到 該實例
- 存在 多線程訪問 的情況
1.2 ThreadLocal 的使用
ThreadLocal
的API
很簡單宋彼,它包含以下四個簽名:
-
get
:獲取ThreadLocal
中當(dāng)前線程共享變量的值。 -
set
:設(shè)置ThreadLocal
中當(dāng)前線程共享變量的值仙畦。 -
remove
:移除ThreadLocal
中當(dāng)前線程共享變量的值宙暇。 -
initialValue
:ThreadLocal
沒有被當(dāng)前線程賦值時或當(dāng)前線程剛調(diào)用remove
方法后調(diào)用get
方法,返回此方法值议泵。
我們用下面的一小段例子,來熟悉一下ThreadLocal
的使用桃熄。
class ThreadLocalSamples {
private static ThreadLocal<Integer> sThreadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 5;
}
};
static void startSample() {
for (int i = 0; i < 3; i++) {
new SampleThread("thread_" + i).start();
}
}
private static class SampleThread extends Thread {
private String mThreadName;
SampleThread(String threadName) {
mThreadName = threadName;
}
@Override
public void run() {
for (int j = 0; j < 5; j++) {
try {
long sleep = (long) (Math.random() * 50);
Thread.sleep(sleep);
int result = sThreadLocal.get();
sThreadLocal.set(++result);
Log.d("ThreadLocalSamples", "ThreadName=" + mThreadName + ",result=" + result);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
運行結(jié)果:
從打印的結(jié)果可以看到先口,雖然這
3
個線程訪問是同一個ThreadLocal
實例,但是它們通過ThreadLocal
的get/set
方法讀寫的并不是同一個實例瞳收,所以保證了在多線程環(huán)境下的獨立性碉京。
二、源碼
2.1 源碼實現(xiàn)
為了加深對于ThreadLocal
的理解螟深,我們來分析一下它的內(nèi)部實現(xiàn)谐宙。ThreadLocal
設(shè)計的核心思想就是:每一個Thread
維護一個ThreadLocalMap
,ThreadLocalMap
的key
是ThreadLocal
界弧,而value
就是真正要存儲的Object
凡蜻。這種方案設(shè)計的優(yōu)點是:
- 每個
Map
的Entry
數(shù)量變小了搭综,之前是Thread
的數(shù)量,現(xiàn)在是ThreadLocal
的數(shù)量划栓,能提高性能兑巾。 - 當(dāng)
Thread
銷毀之后對應(yīng)的ThreadLocalMap
也就隨之銷毀了,能減少內(nèi)存使用量忠荞。
我們先來看一下set
和get
的主要流程:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return 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 getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
寫入的流程為:
- 通過靜態(tài)方法
currentThread
獲取當(dāng)前執(zhí)行指令的線程蒋歌。 - 得到該線程的私有成員變量
threadLocals
,其類型為ThreadLocalMap
委煤,如果沒有創(chuàng)建那么就先創(chuàng)建堂油。 - 通過
ThreadLocalMap
的set
方法存入實際的Object
,其key
值為ThreadLocal
實例碧绞。
讀取的流程為:
- 通過靜態(tài)方法
currentThread
獲取當(dāng)前執(zhí)行指令的線程府框,然后獲取和該線程關(guān)聯(lián)的ThreadLocalMap
- 以
ThreadLocal
實例為key
值,通過ThreadLocalMap
的getEntry
方法找到Object
头遭,如果找到就直接返回寓免;如果沒有找到就調(diào)用setInitialValue
方法,該方法會調(diào)用到我們重寫的initialValue
來嘗試獲取一個初始值计维。
總結(jié)下來就是:ThreadLocal
將一個共用的ThreadLocal
靜態(tài)實例作為key
袜香,將不同對象的引用保存到不同線程的ThreadLocalMap
中,然后在線程執(zhí)行的各處通過這個靜態(tài)ThreadLocal
實例的get()
方法取得自己線程保存的那個對象鲫惶,避免了將這個對象作為參數(shù)傳遞的麻煩蜈首。
它之所以可保證 多線程環(huán)境下的相互獨立,原因在于:每個線程中都有一個自己的ThreadLocalMap
類對象欠母,可以將線程自己的對象保持到其中欢策,線程可以正確的訪問到自己的對象。
當(dāng)然赏淌,這種 獨立性必須要基于一個前提:通過set
方法存儲的對象并不是多個線程共享的踩寇。如果是共享的,那么多個線程get
出來的是同一個是實例六水,仍然會存在多線程問題俺孙。
2.2 ThreadLocalMap
ThreadLocalMap
是ThreadLocal
中的一個內(nèi)部類,與HashMap
類似掷贾,它也會遇到Hash
沖突的問題睛榄,HashMap
采用了 鏈地址法 解決沖突,而ThreadLocalMap
則采用 開放尋址法 解決沖突想帅。
關(guān)于ThreadLocalMap
還有一個疑問场靴,就是它有可能會出現(xiàn)內(nèi)存泄漏,原因是:ThreadLocalMap
的key
值保存的是ThreadLocal
的弱引用,假如ThreadLocal
被回收旨剥,那么就會無法通過Key
找到Object
咧欣,假如線程一直沒有結(jié)束,那么這些Object
就永遠不會被回收泞边。
在ThreadLocalMap
內(nèi)部對于這種情況做了優(yōu)化该押,就是在getEntry
和set
方法查找存儲位置的時候,如果發(fā)現(xiàn)了key
為null
的槽阵谚,那么會將這些槽中對應(yīng)的Object
引用置為null
蚕礼。這并不能解決所有問題,對于使用者來說梢什,可以做額外的兩項優(yōu)化操作:
- 手動調(diào)用
ThreadLocal
的remove
函數(shù)奠蹬,刪除不再需要的ThreadLocal
- 將
ThreadLocal
聲明為private static
的,使得ThreadLocal
的生命周期更長嗡午。
參考文獻
(1) 正確理解 ThreadLocal
(2) 深入剖析 ThreadLocal 實現(xiàn)原理以及內(nèi)存泄漏問題
(3) ThreadLocal 和 synchronized 的區(qū)別