趣頭條面試題:ThreadLocal是什么?怎么用?為什么用它?有什么缺點(diǎn)

作者:Sicimike
blog.csdn.net/Baisitao_/article/details/100063561
前言
相信很多同學(xué)都聽(tīng)過(guò)ThreadLocal,即使沒(méi)用過(guò)也聽(tīng)過(guò)。但是要仔細(xì)一問(wèn)ThreadLocal是個(gè)啥,很多同學(xué)也不一定能說(shuō)清楚。本篇博客就是為了回答關(guān)于ThreadLocal的一系列靈魂拷問(wèn):ThreadLocal是個(gè)什么?怎么用?為什么要用它?它有什么缺點(diǎn)?怎么避免…
ThreadLoacl是什么
在了解ThreadLocal之前,我們先了解下什么是線(xiàn)程封閉
把對(duì)象封閉在一個(gè)線(xiàn)程里,即使這個(gè)對(duì)象不是線(xiàn)程安全的,也不會(huì)出現(xiàn)并發(fā)安全問(wèn)題。
實(shí)現(xiàn)線(xiàn)程封閉大致有三種方式:
Ad-hoc線(xiàn)程封閉:維護(hù)線(xiàn)程封閉性的職責(zé)完全由程序來(lái)承擔(dān),不推薦使用
棧封閉:就是用棧(stack)來(lái)保證線(xiàn)程安全
public void testThread() {
StringBuilder sb = new StringBuilder();
sb.append("Hello");
}
StringBuilder是線(xiàn)程不安全的,但是它只是個(gè)局部變量,局部變量存儲(chǔ)在虛擬機(jī)棧,虛擬機(jī)棧是線(xiàn)程隔離的,所以不會(huì)有線(xiàn)程安全問(wèn)題
ThreadLocal線(xiàn)程封閉:簡(jiǎn)單易用
第三種方式就是通過(guò)ThreadLocal來(lái)實(shí)現(xiàn)線(xiàn)程封閉,線(xiàn)程封閉的指導(dǎo)思想是封閉,而不是共享。所以說(shuō)ThreadLocal是用來(lái)解決變量共享的并發(fā)安全問(wèn)題,多少有些不精確。
使用
JDK1.2開(kāi)始提供的java.lang.ThreadLocal的使用方式非常簡(jiǎn)單
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
final ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("main-thread : Hello");
Thread thread = new Thread(() -> {
// 獲取不到主線(xiàn)程設(shè)置的值,所以為null
System.out.println(threadLocal.get());
threadLocal.set("sub-thread : World");
System.out.println(threadLocal.get());
});
// 啟動(dòng)子線(xiàn)程
thread.start();
// 讓子線(xiàn)程先執(zhí)行完成,再繼續(xù)執(zhí)行主線(xiàn)
thread.join();
// 獲取到的是主線(xiàn)程設(shè)置的值,而不是子線(xiàn)程設(shè)置的
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println(threadLocal.get());
}
}
運(yùn)行結(jié)果
null
sub-thread : World
main-thread : Hello
null
運(yùn)行結(jié)果說(shuō)明了ThreadLocal只能獲取本線(xiàn)程設(shè)置的值,也就是線(xiàn)程封閉。基本上,ThreadLocal對(duì)外提供的方法只有三個(gè)get()、set(T)、remove()。
原理
使用方式非常簡(jiǎn)單,所以我們來(lái)看看ThreadLocal的源碼。ThreadLocal內(nèi)部定義了一個(gè)靜態(tài)ThreadLocalMap類(lèi),ThreadLocalMap內(nèi)部又定義了一個(gè)Entry類(lèi),這里只看一些主要的屬性和方法
public class ThreadLocal<T> {
public T get() {
Thread t = Thread.currentThread();
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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
// 從這里可以看出ThreadLocalMap對(duì)象是被Thread類(lèi)持有的
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 內(nèi)部類(lèi)ThreadLocalMap
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
// 內(nèi)部類(lèi)Entity,實(shí)際存儲(chǔ)數(shù)據(jù)的地方
// Entry的key是ThreadLocal對(duì)象,不是當(dāng)前線(xiàn)程ID或者名稱(chēng)
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 注意這里維護(hù)的是Entry數(shù)組
private Entry[] table;
}
}
根據(jù)上面的源碼,可以大致畫(huà)出ThreadLocal在虛擬機(jī)內(nèi)存中的結(jié)構(gòu)

實(shí)線(xiàn)箭頭表示強(qiáng)引用,虛線(xiàn)箭頭表示弱引用(關(guān)于對(duì)象的四種引用,可以參考博主之前的博客:Java中四種引用)。需要注意的是:
ThreadLocalMap雖然是在ThreadLocal類(lèi)中定義的,但是實(shí)際上被Thread持有。 Entry的key是(虛引用的)ThreadLocal對(duì)象,而不是當(dāng)前線(xiàn)程ID或者線(xiàn)程名稱(chēng)。 ThreadLocalMap中持有的是Entry數(shù)組,而不是Entry對(duì)象。
對(duì)于第一點(diǎn),ThreadLocalMap被Thread持有是為了實(shí)現(xiàn)每個(gè)線(xiàn)程都有自己獨(dú)立的ThreadLocalMap對(duì)象,以此為基礎(chǔ),做到線(xiàn)程隔離。第二點(diǎn)和第三點(diǎn)理解,我們先來(lái)想一個(gè)問(wèn)題,如果同一個(gè)線(xiàn)程中定義了多個(gè)ThreadLocal對(duì)象,內(nèi)存結(jié)構(gòu)應(yīng)該是怎樣的?此時(shí)再來(lái)看一下ThreadLocal.set(T)方法:
public void set(T value) {
// 獲取當(dāng)前線(xiàn)程對(duì)象
Thread t = Thread.currentThread();
// 根據(jù)線(xiàn)程對(duì)象獲取ThreadLocalMap對(duì)象(ThreadLocalMap被Thread持有)
ThreadLocalMap map = getMap(t);
// 如果ThreadLocalMap存在,則直接插入;不存在,則新建ThreadLocalMap
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
也就是說(shuō),如果程序定義了多個(gè)ThreadLocal,會(huì)共用一個(gè)ThreadLocalMap對(duì)象,所以?xún)?nèi)存結(jié)構(gòu)應(yīng)該是這樣

這個(gè)內(nèi)存結(jié)構(gòu)圖解釋了第二點(diǎn)和第三點(diǎn)。假設(shè)Entry中key為當(dāng)前線(xiàn)程ID或者名稱(chēng)的話(huà),那么程序中定義多個(gè)ThreadLocal對(duì)象時(shí),Entry數(shù)組中的所有Entry的key都一樣(或者說(shuō)只能存一個(gè)value)。ThreadLocalMap中持有的是Entry數(shù)組,而不是Entry,則是因?yàn)槌绦蚩啥x多個(gè)ThreadLocal對(duì)象,自然需要一個(gè)數(shù)組。
內(nèi)存泄漏
ThreadLocal會(huì)發(fā)生內(nèi)存泄漏嗎?
會(huì)
仔細(xì)看下ThreadLocal內(nèi)存結(jié)構(gòu)就會(huì)發(fā)現(xiàn),Entry數(shù)組對(duì)象通過(guò)ThreadLocalMap最終被Thread持有,并且是強(qiáng)引用。也就是說(shuō)Entry數(shù)組對(duì)象的生命周期和當(dāng)前線(xiàn)程一樣。即使ThreadLocal對(duì)象被回收了,Entry數(shù)組對(duì)象也不一定被回收,這樣就有可能發(fā)生內(nèi)存泄漏。ThreadLocal在設(shè)計(jì)的時(shí)候就提供了一些補(bǔ)救措施:
Entry的key是弱引用的ThreadLocal對(duì)象,很容易被回收,導(dǎo)致key為null(但是value不為null)。所以在調(diào)用get()、set(T)、remove()等方法的時(shí)候,會(huì)自動(dòng)清理key為null的Entity。 remove()方法就是用來(lái)清理無(wú)用對(duì)象,防止內(nèi)存泄漏的。所以每次用完ThreadLocal后需要手動(dòng)remove()。
有些文章認(rèn)為是弱引用導(dǎo)致了內(nèi)存泄漏,其實(shí)是不對(duì)的。假設(shè)把弱引用變成強(qiáng)引用,這樣無(wú)用的對(duì)象key和value都不為null,反而不利于GC,只能通過(guò)remove()方法手動(dòng)清理,或者等待線(xiàn)程結(jié)束生命周期。也就是說(shuō)ThreadLocalMap的生命周期由持有它的線(xiàn)程來(lái)決定,線(xiàn)程如果不進(jìn)入terminated狀態(tài),ThreadLocalMap就不會(huì)被GC回收,這才是ThreadLocal內(nèi)存泄露的原因。
應(yīng)用場(chǎng)景
維護(hù)JDBC的java.sql.Connection對(duì)象,因?yàn)槊總€(gè)線(xiàn)程都需要保持特定的Connection對(duì)象。 Web開(kāi)發(fā)時(shí),有些信息需要從controller傳到service傳到dao,甚至傳到util類(lèi)??雌饋?lái)非常不優(yōu)雅,這時(shí)便可以使用ThreadLocal來(lái)優(yōu)雅的實(shí)現(xiàn)。 包括線(xiàn)程不安全的工具類(lèi),比如Random、SimpleDateFormat等
與synchronized的關(guān)系
有些文章拿ThreadLocal和synchronized比較,其實(shí)它們的實(shí)現(xiàn)思想不一樣。
synchronized是同一時(shí)間最多只有一個(gè)線(xiàn)程執(zhí)行,所以變量只需要存一份,算是一種時(shí)間換空間的思想 ThreadLocal是多個(gè)線(xiàn)程互不影響,所以每個(gè)線(xiàn)程存一份變量,算是一種空間換時(shí)間的思想
總結(jié)
ThreadLocal是一種隔離的思想,當(dāng)一個(gè)變量需要進(jìn)行線(xiàn)程隔離時(shí),就可以考慮使用ThreadLocal來(lái)優(yōu)雅的實(shí)現(xiàn)。
Springboot啟動(dòng)擴(kuò)展點(diǎn)超詳細(xì)總結(jié),再也不怕面試官問(wèn)了 List去除重復(fù)數(shù)據(jù)的五種方式,學(xué)到了... SpringBoot操作ES進(jìn)行各種高級(jí)查詢(xún)(必須收藏) 10w行級(jí)別數(shù)據(jù)的Excel導(dǎo)入優(yōu)化記錄 再見(jiàn),HttpClient!再見(jiàn),Okhttp! 使用Docker部署SpringBoot+Vue系統(tǒng) 快試試Java8中的StringJoiner吧,真香!

