哥,我還是不懂 ThreadLocal
大家好,我是風(fēng)箏
前幾天群里有個(gè)弟弟說(shuō)看 TheadLocal 有點(diǎn)懵,我就把之前寫(xiě)的那篇給他扔過(guò)去了,結(jié)果他看完了跟我說(shuō):哥,我還是沒(méi)看懂?。?/p>
什么,這意思就是我寫(xiě)的那篇文章不行啊,看完了也看不懂,這怎么能行。于是我問(wèn)他現(xiàn)在糾結(jié)在哪里了,啥地方不懂。經(jīng)過(guò)一番溝通,我發(fā)現(xiàn)那篇文章確實(shí)寫(xiě)得不太行,好多新手不理解的點(diǎn)都沒(méi)有點(diǎn)出來(lái)。
具體的一些容易讓人迷糊的點(diǎn)有以下幾個(gè),雖然有一些問(wèn)題看起來(lái)很傻,但是它們確實(shí)存在。
- ThreadLocal 存的值在不同線程間怎么傳遞?
- ThreadLocal 以什么形式存儲(chǔ)?
- ThreadLocal 可不可以放多個(gè)值?
- ThreadLocal 到底是存在哪?跟線程有什么關(guān)系?
咱們上來(lái)先看一段代碼精神精神,接下來(lái)再一一解釋上面的問(wèn)題。這段代碼中聲明了兩個(gè) ThreadLocal ,然后在線程0和線程1中分別賦值這兩個(gè) ThreadLocal,第三個(gè)線程不賦值,在每個(gè)線程中打印這兩個(gè) ThreadLocal 的值。
看一下應(yīng)該輸出的值是多少。
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> threadLocal1 = ThreadLocal.withInitial(() -> "啥都沒(méi)干,初始值");
ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
Thread thread0 = new Thread() {
@Override
public void run() {
threadLocal1.set("我是threadLocal1 「Thread0」給我賦的值");
threadLocal2.set("我是threadLocal2 「Thread0」給我賦的值");
String name = "Thread0-";
System.out.println(name + "threadLocal1 = " + threadLocal1.get());
System.out.println(name + "threadLocal2 = " + threadLocal2.get());
}
};
thread0.start();
thread0.join();
System.out.println();
Thread thread1 = new Thread() {
@Override
public void run() {
threadLocal1.set("我是threadLocal1 「Thread1」給我賦的值");
threadLocal2.set("我是threadLocal2 「Thread1」給我賦的值");
String name = "Thread1-";
System.out.println(name + "threadLocal1 = " + threadLocal1.get());
System.out.println(name + "threadLocal = " + threadLocal2.get());
}
};
thread1.start();
thread1.join();
System.out.println();
Thread thread2 = new Thread() {
@Override
public void run() {
String name = "Thread2-";
System.out.println(name + "threadLocal1 = " + threadLocal1.get());
}
};
thread2.start();
}
下面是輸出的值,看看是不是和你理解的一致。
“
Thread0-threadLocal1 = 我是threadLocal1 「Thread0」給我賦的值 Thread0-threadLocal2 = 我是threadLocal2 「Thread0」給我賦的值
Thread1-threadLocal1 = 我是threadLocal1 「Thread1」給我賦的值 Thread1-threadLocal = 我是threadLocal2 「Thread1」給我賦的值
Thread2-threadLocal1 = 啥都沒(méi)干,初始值
如果和你想的輸出是一樣的,那你可能已經(jīng)理解了 TheadLocal 了,如果不一致的話,那說(shuō)明你還沒(méi)有掌握它。
問(wèn)題1:ThreadLocal 存的值在不同線程間怎么傳遞?
我聽(tīng)到這個(gè)問(wèn)題有些詫異了,你真是一點(diǎn)兒都沒(méi)懂啊。ThreadLocal 當(dāng)然不需要在進(jìn)程間傳遞了,ThreadLocal 的初衷就是為了不在進(jìn)程間傳遞值,而只是在當(dāng)前線程的各個(gè)地方都能獲取到。
這就要說(shuō)到 ThreadLocal 的定義和應(yīng)用場(chǎng)景了。
ThreadLocal 定義以及使用場(chǎng)景
ThreadLocal允許每個(gè)線程獨(dú)立存儲(chǔ)和訪問(wèn)線程本地變量。線程本地變量是指每個(gè)線程都有自己獨(dú)立的變量副本,互不干擾。這對(duì)于多線程編程來(lái)說(shuō)非常有用,因?yàn)樗试S在每個(gè)線程中存儲(chǔ)狀態(tài)或數(shù)據(jù),而不需要擔(dān)心線程間的競(jìng)爭(zhēng)條件。
我們進(jìn)到 ThreadLocal 的源碼中,通過(guò)源碼注釋就可以看到很清晰的解釋:它是線程的局部變量,這些變量只能在這個(gè)線程內(nèi)被讀寫(xiě),在其他線程內(nèi)是無(wú)法訪問(wèn)的。ThreadLocal 定義的通常是與線程關(guān)聯(lián)的私有靜態(tài)字段(例如,用戶ID或事務(wù)ID)。
變量有局部的還有全局的,局部變量沒(méi)什么好說(shuō)的,一涉及到全局,那自然就會(huì)出現(xiàn)多線程的安全問(wèn)題,要保證多線程安全訪問(wèn),不出現(xiàn)臟讀臟寫(xiě),那就要涉及到線程同步了。而 ThreadLocal 相當(dāng)于提供了介于局部變量與全局變量中間的這樣一種線程內(nèi)部的全局變量。
根據(jù) ThreadLocal 的定義,我們就可以知道它的使用場(chǎng)景了。就是當(dāng)我們只想在本身的線程內(nèi)使用的變量,比如這個(gè)線程要存活一段時(shí)間,可以用 ThreadLocal 來(lái)實(shí)現(xiàn),并且這些變量是和線程的生命周期密切相關(guān)的,線程結(jié)束,變量也就銷毀了。
舉幾個(gè)例子說(shuō)明一下:
1、比如線程中處理一個(gè)非常復(fù)雜的業(yè)務(wù),可能方法有很多,那么,使用 ThreadLocal 可以代替一些參數(shù)的顯式傳遞;
2、比如用來(lái)存儲(chǔ)用戶 Session。Session 的特性很適合 ThreadLocal ,因?yàn)?Session 之前當(dāng)前會(huì)話周期內(nèi)有效,會(huì)話結(jié)束便銷毀。我們先籠統(tǒng)的分析一次 web 請(qǐng)求的過(guò)程:
- 用戶在瀏覽器中訪問(wèn) web 頁(yè)面;
- 瀏覽器向服務(wù)器發(fā)起請(qǐng)求;
- 服務(wù)器上的服務(wù)處理程序(例如tomcat)接收請(qǐng)求,并開(kāi)啟一個(gè)線程處理請(qǐng)求,期間會(huì)使用到 Session ;
- 最后服務(wù)器將請(qǐng)求結(jié)果返回給客戶端瀏覽器。
從這個(gè)簡(jiǎn)單的訪問(wèn)過(guò)程我們看到正好這個(gè) Session 是在處理一個(gè)用戶會(huì)話過(guò)程中產(chǎn)生并使用的,如果單純的理解一個(gè)用戶的一次會(huì)話對(duì)應(yīng)服務(wù)端一個(gè)獨(dú)立的處理線程,那用 ThreadLocal 在存儲(chǔ) Session ,簡(jiǎn)直是再合適不過(guò)了。但是例如 tomcat 這類的服務(wù)器軟件都是采用了線程池技術(shù)的,并不是嚴(yán)格意義上的一個(gè)會(huì)話對(duì)應(yīng)一個(gè)線程。并不是說(shuō)這種情況就不適合 ThreadLocal 了,而是要在每次請(qǐng)求進(jìn)來(lái)時(shí)先清理掉之前的 Session ,一般可以用攔截器、過(guò)濾器來(lái)實(shí)現(xiàn)。
3、在一些多線程的情況下,如果用線程同步的方式,當(dāng)并發(fā)比較高的時(shí)候會(huì)影響性能,可以改為 ThreadLocal 的方式,例如高性能序列化框架 Kyro 就要用 ThreadLocal 來(lái)保證高性能和線程安全;
4、還有像線程內(nèi)上線文管理器、數(shù)據(jù)庫(kù)連接等可以用到 ThreadLocal;
使用方式
ThreadLocal 的使用非常簡(jiǎn)單,最核心的操作就是四個(gè):創(chuàng)建、創(chuàng)建并賦初始值、賦值、取值。
1、創(chuàng)建
ThreadLocal<String> mLocal = new ThreadLocal<>();
2、創(chuàng)建并賦初值。下面代碼表示創(chuàng)建了一個(gè) String 類型的 ThreadLocal 并且重寫(xiě)了 initialValue 方法,并返回初始字符串,之后調(diào)用 get() 方法獲取的值便是 initialValue 方法返回的值。
ThreadLocal<String> mLocal = new ThreadLocal<String>(){
@Override
protected String initialValue(){
return "init value";
}
};
System.out.println(mLocal.get());
3、設(shè)置值
mLocal.set("hello");
4、取值
mLocal.get()
實(shí)現(xiàn)原理
前面回答了第一個(gè)問(wèn)題,后面的三個(gè)問(wèn)題就涉及到 ThreadLocal 的原理了。
首先 ThreadLocal 是一個(gè)泛型類,保證可以接受任何類型的對(duì)象,所以你可以在 ThreadLocal 中放基本類型,比如字符串、整型等,也可以放自定義的實(shí)體對(duì)象,還可以放 List、Set、Map 等都沒(méi)有問(wèn)題。
圖片
先來(lái)理清楚 ThreadLocal 對(duì)象的結(jié)構(gòu)與線程的關(guān)系,我解釋一下上圖的意思。
- 在 Thread 類中有一個(gè)屬性叫做 threadLocals,這個(gè)屬性的類型是 ThreadLocal.ThreadLocalMap 類型;
- ThreadLocal 就是我們會(huì)直接用到的 ThreadLocal 對(duì)象;
- ThreadLocal 有個(gè)內(nèi)部類 是 ThreadLocalMap,就是 Thread 類中的的 threadLocals 對(duì)象的類型;
- ThreadLocalMap 通過(guò)名稱可以看出這是一個(gè) Map 結(jié)構(gòu),如果你看過(guò) HashMap 的實(shí)現(xiàn),就會(huì)發(fā)現(xiàn)它是個(gè)簡(jiǎn)易版的 HashMap;
- ThreadLocalMap 中真正存儲(chǔ)數(shù)據(jù)的是一個(gè) Entry 數(shù)組;
- Entry 又是ThreadLocalMap的一個(gè)靜態(tài)內(nèi)部類, 它繼承 WeakReference 弱引用,暫且理解為是一個(gè) key-value 鍵值對(duì);其中涉及的重要對(duì)象大概就是上面這些,了解這些基礎(chǔ)后,能幫我們更清楚的理解原理。
看上去可能有點(diǎn)亂,最簡(jiǎn)單的就是從 set 方法入手看一看。下面是 set 方法代碼
public void set(T value) {
Thread t = Thread.currentThread(); // 獲取當(dāng)前線程
ThreadLocalMap map = getMap(t); // 獲取當(dāng)前線程維護(hù)的 threadLocals
if (map != null)
map.set(this, value); // 如果 map 不為空,直接添加
else
createMap(t, value); //如果 map 為空,先初始化,再添加
}
調(diào)用 set 方法
ThreadLocal<String> mLocal = new ThreadLocal<>();
mLocal.set("hello");
調(diào)用 ThreadLocal 的 set 方法時(shí),首先獲取到了當(dāng)前線程。
Thread t = Thread.currentThread();
然后獲取當(dāng)前線程維護(hù)的 ThreadLocalMap 對(duì)象。通過(guò) getMap() 方法,t 就是當(dāng)前線程,直接返回當(dāng)前線程中的 threadLocals 屬性。
ThreadLocalMap map = getMap(t); //獲取 ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
如果 map 不為null,說(shuō)明之前設(shè)置過(guò) ThreadLocal 了,那就調(diào)用ThreadLocalMap 的set 方法。
private void set(ThreadLocal<?> key, Object value) {
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();
}
int i = key.threadLocalHashCode & (len-1);
計(jì)算索引,tab[i]就是要存儲(chǔ)的位置,后面 for 中的部分就是處理哈希沖突和更新已有值,先不用管這些細(xì)節(jié),之后將 new Entry(key, value)放到 tab[i]的位置,也就是放到 Entry 數(shù)組中了。
這里面 new Entry中的參數(shù) key 和 value 很關(guān)鍵。返回去看 ThreadLocalMap.set 方法調(diào)用時(shí)候的傳參。
map.set(this, value);
key 是什么呢?key 這里傳的是 this,this 是誰(shuí)呢,就是 ThreadLocal 本身,它本身被當(dāng)做 key 了。value 是什么呢?value 就是調(diào)用 ThreadLocal.set(value)時(shí)傳過(guò)來(lái)的泛型的值,是我們調(diào)用方自己設(shè)置的。
后面還有如果 ThreadLocalMap 實(shí)例不存在的話,則要初始化并賦初值的過(guò)程,這部分也不是理解 ThreadLocal 的重點(diǎn),就不具體講了,看代碼都能理解。
所以后面那三個(gè)問(wèn)題也就解決了。
現(xiàn)在再回過(guò)頭去看最開(kāi)始給的那段代碼。
threadLocal1 和 threadLocal2 的聲明是在 main 方法中的,也就是在主線中聲明的,三個(gè)子線程都可以看到的。
而且線程0和線程1都用了兩個(gè) ThreadLocal,所以說(shuō),一個(gè)線程可以用多個(gè) ThreadLocal,因?yàn)樽罱K存儲(chǔ)實(shí)際上是個(gè) Map,多少個(gè)都沒(méi)關(guān)系。
線程 0 和線程1都對(duì)threadLocal1 和 threadLocal2重新設(shè)置值了,然后通過(guò)get方法得到的也是本線程設(shè)置的值。線程2沒(méi)有對(duì) threadLocal1 賦值,所以在調(diào)用get方法后,得到的是threadLocal1最開(kāi)始設(shè)置的初始值,并不是線程0或線程2設(shè)置的值。也印證了線程之間是不會(huì)互相影響的(當(dāng)然,我們通過(guò)上面的分析已經(jīng)了解這個(gè)原理了)。
內(nèi)存泄漏問(wèn)題
實(shí)際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,弱引用的特點(diǎn)是,如果這個(gè)對(duì)象只存在弱引用,那么在下一次垃圾回收的時(shí)候必然會(huì)被清理掉。
所以如果 ThreadLocal 沒(méi)有被外部強(qiáng)引用的情況下,在垃圾回收的時(shí)候會(huì)被清理掉的,這樣一來(lái) ThreadLocalMap 中使用這個(gè) ThreadLocal 的 key 也會(huì)被清理掉。但是,value 是強(qiáng)引用,不會(huì)被清理,這樣一來(lái)就會(huì)出現(xiàn) key 為 null 的 value。
ThreadLocalMap 實(shí)現(xiàn)中已經(jīng)考慮了這種情況,在調(diào)用 set()、get()、remove() 方法的時(shí)候,會(huì)清理掉 key 為 null 的記錄。如果說(shuō)會(huì)出現(xiàn)內(nèi)存泄漏,那只有在出現(xiàn)了 key 為 null 的記錄后,沒(méi)有手動(dòng)調(diào)用 remove() 方法,并且之后也不再調(diào)用 get()、set()、remove() 方法的情況下。
這回,理解了嗎?