Java 面試高頻 ThreadLocal
面試題
- ThreadLocal中ThreadLocalMap的數(shù)據(jù)結(jié)構(gòu)和關(guān)系?
- ThreadLocal的key是弱引用,這是為什么?
- ThreadLocal內(nèi)存泄露問題你知道嗎?
- ThreadLocal中最后為什么要加remove方法?
是什么? 能干嘛
ThreadLocal提供線程局部變量。這些變量與正常的變量不同,因為每一個線程在訪問ThreadLocal實例的時候(通過其get或set方法)都有自己的、獨立初始化的變量副本。ThreadLocal實例通常是類中的私有靜態(tài)字段,使用它的目的是希望將狀態(tài)(例如,用戶ID或事務(wù)ID)與線程關(guān)聯(lián)起來。
主要解決了讓每個線程綁定自己的值,通過使用get()和set()方法,獲取默認值或?qū)⑵渲蹈臑楫?dāng)前線程所存的副本的值從而避免了線程安全問題。
一句話如何才能不爭搶
- 加入synchronized或者Lock控制資源的訪問順序
- 人手一份,大家各自安好,沒必要搶奪
舉個栗子-阿里規(guī)范
為什么SimpleDateFormat是線程不安全的?
官方文檔說明
測試
public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 模擬并發(fā)環(huán)境下使用SimpleDateFormat的parse方法將字符串轉(zhuǎn)換成Date對象
* @param stringDate
* @return
* @throws Exception
*/
public static Date parseDate(String stringDate)throws Exception {
return sdf.parse(stringDate);
}
public static void main(String[] args) throws Exception {
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
try {
System.out.println(DateUtils.parseDate("2020-11-11 11:11:11"));
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
源碼分析
SimpleDateFormat類內(nèi)部有一個Calendar對象引用,它用來儲存和這個SimpleDateFormat相關(guān)的日期信息。
例如sdf.parse(dateStr),sdf.format(date) 諸如此類的方法參數(shù)傳入的日期相關(guān)String,Date等等, 都是交由Calendar引用來儲存的。這樣就會導(dǎo)致一個問題:如果你的SimpleDateFormat是個static的, 那么多個thread 之間就會共享這個SimpleDateFormat, 同時也是共享這個Calendar引用。
SimpleDataFormat的parse()方法會先調(diào)用Calendar.clear(),然后調(diào)用Calendar.add(),如果一個線程先調(diào)用了add()然后另一個線程又調(diào)用了clear(),這時候parse()方法解析的時間就不對了。
舉個例子:
假設(shè)線程 A 剛執(zhí)行完 calendar.setTime(date) 語句,把時間設(shè)置為 2020-09-01,但線程還沒執(zhí)行完,線程 B 又執(zhí)行了 calendar.setTime(date) 語句,把時間設(shè)置為 2020-09-02,這個時候就出現(xiàn)幻讀了,線程 A 繼續(xù)執(zhí)行下去的時候,拿到的 calendar.getTime 得到的時間就是線程B改過之后的。
如何解決
- 解決1
將SimpleDateFormat定義成局部變量。
缺點:每調(diào)用一次方法就會創(chuàng)建一個SimpleDateFormat對象,方法結(jié)束又要作為垃圾回收。
public static void main(String[] args) throws Exception {
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//System.out.println(DateUtils.parseDate("2020-11-11 11:11:11"));
System.out.println(sdf.parse("2020-11-11 11:11:11"));
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
- 解決2
ThreadLocal,也叫做線程本地變量或者線程本地存儲
private static final ThreadLocal sdf_threadLocal =
ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
/**
* ThreadLocal可以確保每個線程都可以得到各自單獨的一個SimpleDateFormat的對象,那么自然也就不存在競爭問題了。
* @param stringDate
* @return
* @throws Exception
*/
public static Date parseDateTL(String stringDate)throws Exception {
return sdf_threadLocal.get().parse(stringDate);
}
public static void main(String[] args) throws Exception {
for (int i = 1; i < 30; i++) {
new Thread(() -> {
try {
System.out.println(DateUtils.parseDateTL("2020-11-11 11:11:11"));
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
- 其它
加鎖
第3方時間庫
ThreadLocal Thread ThreadLocalMap 之間的關(guān)系
ThreadLocal :每個線程通過此對象都會返回各自的值,互不干擾,這是因為每個線程都存著自己的一份副本。需要注意的是線程結(jié)束后,它所保存的所有副本都將進行垃圾回收(除非存在對這些副本的其他引用)
ThreadLocal的get操作是這樣執(zhí)行的:ThreadLocalMap map = thread.threadLocals -> return map.getEntry(threadLocal)ThreadLocal的set操作是這樣執(zhí)行的:ThreadLocalMap map = thread.threadLocals -> map.set(threadLocal, value)
三者的關(guān)系是:
- 每個Thread對應(yīng)的所有ThreadLocal副本都存放在ThreadLocalMap對象中,key是ThreadLocal,value是副本數(shù)據(jù)。
- ThreadLocalMap對象存放在Thread對象中。
- 通過ThreadLocal獲取副本數(shù)據(jù)時,實際是通過訪問Thread來獲取ThreadLocalMap,再通過ThreadLocalMap獲取副本數(shù)據(jù)。
ThreadLocal內(nèi)存泄露問題
阿里手冊
什么是內(nèi)存泄漏
不再會被使用的對象或者變量占用的內(nèi)存不能被回收,就是內(nèi)存泄露。
ThreadLocal在保存的時候會把自己當(dāng)做Key存在ThreadLocalMap中,正常情況應(yīng)該是key和value都應(yīng)該被外界強引用才對,但是現(xiàn)在key被設(shè)計成WeakReference弱引用了。
強引用、軟引用、虛引用、弱引用
強引用
當(dāng)內(nèi)存不足,jvm開始垃圾回收,對于強引用的對象,就算是出現(xiàn)了OOM也不會對該對象進行回收。這也是Java中最常見的普通對象的引用,只要還有強引用指向這個對象,就不會被垃圾回收。當(dāng)這個對象沒有了其他的引用關(guān)系,只要是超過了引用的作用域,或者顯示的將強引用賦值為null,一般就可以進行垃圾回收了。
軟引用
軟引用是相對強引用弱化了一些的引用,對于軟引用的對象來說:
- 當(dāng)內(nèi)存充足時,它不會被回收。
- 當(dāng)內(nèi)存不足時。會被回收。
通常用在對內(nèi)存敏感的程序中,就像高速緩存。
弱引用
發(fā)現(xiàn)即回收
弱引用也是用來描述那些非必需對象,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生為止。在系統(tǒng)GC時,只要發(fā)現(xiàn)弱引用,不管系統(tǒng)堆空間使用是否充足,都會回收掉只被弱引用關(guān)聯(lián)的對象。
但是,由于垃圾回收器的線程通常優(yōu)先級很低,因此,并不一定能很快地發(fā)現(xiàn)持有弱引用的對象。在這種情況下,弱引用對象可以存在較長的時間。弱引用和軟引用一樣,在構(gòu)造弱引用時,也可以指定一個引用隊列,當(dāng)弱引用對象被回收時,就會加入指定的引用隊列,通過這個隊列可以跟蹤對象的回收情況。軟引用、弱引用都非常適合來保存那些可有可無的緩存數(shù)據(jù)。如果這么做,當(dāng)系統(tǒng)內(nèi)存不足時,這些緩存數(shù)據(jù)會被回收,不會導(dǎo)致內(nèi)存溢出。而當(dāng)內(nèi)存資源充足時,這些緩存數(shù)據(jù)又可以存在相當(dāng)長的時間,從而起到加速系統(tǒng)的作用。在JDK1.2版之后提供了WeakReference類來實現(xiàn)弱引用
// 聲明強引用
Object obj = new Object();
// 創(chuàng)建一個弱引用
WeakReference<Object> sf = new WeakReference<>(obj);
obj = null; //銷毀強引用,這是必須的,不然會存在強引用和弱引用
虛引用
也稱為“幽靈引用”或者“幻影引用”,是所有引用類型中最弱的一個
一個對象是否有虛引用的存在,完全不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它和沒有引用幾乎是一樣的,隨時都可能被垃圾回收器回收。它不能單獨使用,也無法通過虛引用來獲取被引用的對象。當(dāng)試圖通過虛引用的get()方法取得對象時,總是null。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的在于跟蹤垃圾回收過程。比如:能在這個對象被收集器回收時收到一個系統(tǒng)通知。
虛引用必須和引用隊列一起使用。虛引用在創(chuàng)建時必須提供一個引用隊列作為參數(shù)。當(dāng)垃圾回收器準(zhǔn)備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象后,將這個虛引用加入引用隊列,以通知應(yīng)用程序?qū)ο蟮幕厥涨闆r。由于虛引用可以跟蹤對象的回收時間,因此,也可以將一些資源釋放操作放置在虛引用中執(zhí)行和記錄。在JDK1.2版之后提供了PhantomReference類來實現(xiàn)虛引用。
// 聲明強引用
Object obj = new Object();
// 聲明引用隊列
ReferenceQueue phantomQueue = new ReferenceQueue();
// 聲明虛引用(還需要傳入引用隊列)
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null;
內(nèi)存泄漏問題
回到ThreadLocal這邊來說:ThreadLocal在沒有外部強引用時,發(fā)生GC時會被回收,如果創(chuàng)建ThreadLocal的線程一直持續(xù)運行,那么這個Entry對象中的value就有可能一直得不到回收,發(fā)生內(nèi)存泄露。
就比如線程池里面的線程,線程都是復(fù)用的,那么之前的線程實例處理完之后,出于復(fù)用的目的線程依然存活,所以,ThreadLocal設(shè)定的value值被持有,導(dǎo)致內(nèi)存泄露。
按照道理一個線程使用完,ThreadLocalMap是應(yīng)該要被清空的,但是現(xiàn)在線程被復(fù)用了。
那怎么解決?
在代碼的最后使用remove就好了,我們只要記得在使用的最后用remove把值清空就好了。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("張三");
……
} finally {
localName.remove();
}
remove的源碼很簡單,找到對應(yīng)的值全部置空,這樣在垃圾回收器回收的時候,會自動把他們回收掉。
那為什么ThreadLocalMap的key要設(shè)計成弱引用?
key不設(shè)置成弱引用的話就會造成和entry中value一樣內(nèi)存泄漏的場景。
補充一點:ThreadLocal的不足,我覺得可以通過看看netty的fastThreadLocal來彌補,大家有興趣可以康康。
key為null的entry的值傳遞的bug
ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用引用他,那么系統(tǒng)gc的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現(xiàn)key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話(比如正好用在線程池),這些key為null的Entry的value就會一直存在一條強引用鏈。
雖然弱引用,保證了key指向的ThreadLocal對象能被及時回收,但是v指向的value對象是需要ThreadLocalMap調(diào)用get、set時發(fā)現(xiàn)key為null時才會去回收整個entry、value,因此弱引用不能100%保證內(nèi)存不泄露。我們要在不使用某個ThreadLocal對象后,手動調(diào)用remoev方法來刪除它,
另外在線程池中,不僅僅是內(nèi)存泄露的問題,因為線程池中的線程是重復(fù)使用的,意味著這個線程的ThreadLocalMap對象也是重復(fù)使用的,如果我們不手動調(diào)用remove方法,那么后面的線程就有可能獲取到上個線程遺留下來的value值,造成bug。--值傳遞的問題
值傳遞的問題
ThreadLocal 無法共享線程中的數(shù)據(jù),InheritableThreadLocal 可以再父子線程之間完成值的傳遞
InheritableThreadLocal中createMap,以及getMap方法處理的對象不一樣了,其中在ThreadLocal中處理的是threadLocals,而InheritableThreadLocal中的是inheritableThreadLocals。Inheritablethreadlocal在父子線程傳遞的時候,依托的是thread的,在執(zhí)行init( )初始化方法。在init方法中會執(zhí)行parent.inheritableThreadLocals,這個方法會把父線程的值賦給子線程,如果我們使用的是線程池,這樣不會再重新執(zhí)行init()初始化方法,而是直接使用已經(jīng)創(chuàng)建過的線程,所以這里的值不會二次產(chǎn)生變化,做不到真正的父子線程數(shù)據(jù)傳遞,就會出現(xiàn)數(shù)據(jù)錯亂的問題。
TransmittableThreadLocal 的原理:它底層使用的TtlRunnable, 在TtlRunnable 構(gòu)造方法中,會獲取當(dāng)前線程中所有的上下文,并儲存在 AtomicReference 中。而且TtlRunnable 是實現(xiàn)于 Runnable,在 TtlRunnable run 方法中會執(zhí)行 Runnable run 方法。當(dāng)線程執(zhí)行時,TtlRunnable 會從 AtomicReference 中獲取出調(diào)用線程中所有的上下文,并把上下文通過 TransmittableThreadLocal.Transmitter.replay 方法把上下文復(fù)制到當(dāng)前線程。這樣就可以在使用線程池的情況下也可以完成值的傳遞了。
ThreadLocalMap的原理
介紹
ThreadLocalMap 是一個自定義map,它并沒有實現(xiàn)Map接口,而且他的Entry是繼承WeakReference(弱引用)的,也沒有看到HashMap中的next,所以不存在鏈表了。
為什么底層用數(shù)組?沒有了鏈表怎么解決Hash沖突呢?
用數(shù)組是因為,我們開發(fā)過程中可以一個線程可以有多個TreadLocal來存放不同類型的對象的,但是他們都將放到你當(dāng)前線程的ThreadLocalMap里,所以肯定要數(shù)組來存。
從源碼里面看到ThreadLocalMap在存儲的時候會給每一個ThreadLocal對象一個threadLocalHashCode,在插入過程中,根據(jù)ThreadLocal對象的hash值,定位到table中的位置i,
int i = key.threadLocalHashCode & (len-1)。然后會判斷一下:如果當(dāng)前位置是空的,就初始化一個Entry對象放在位置i上;如果位置i不為空,如果這個Entry對象的key正好是即將設(shè)置的key,那么就刷新Entry中的value;如果位置i的不為空,而且key不等于entry,那就找下一個空位置,直到為空為止。
小總結(jié)
- ThreadLocal 并不解決線程間共享數(shù)據(jù)的問題
- ThreadLocal 適用于變量在線程間隔離且在方法間共享的場景
- ThreadLocal 通過隱式的在不同線程內(nèi)創(chuàng)建獨立實例副本避免了實例線程安全的問題
- 每個線程持有一個只屬于自己的專屬Map并維護了ThreadLocal對象與具體實例的映射,該Map由于只被持有它的線程訪問,故不存在線程安全以及鎖的問題
- ThreadLocalMap的Entry對ThreadLocal的引用為弱引用,避免了ThreadLocal對象無法被回收的問題