自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Java 面試高頻 ThreadLocal

開發(fā) 前端
ThreadLocalMap 是一個(gè)自定義map,它并沒有實(shí)現(xiàn)Map接口,而且他的Entry是繼承WeakReference(弱引用)的,也沒有看到HashMap中的next,所以不存在鏈表了。

面試題

  • ThreadLocal中ThreadLocalMap的數(shù)據(jù)結(jié)構(gòu)和關(guān)系?
  • ThreadLocal的key是弱引用,這是為什么?
  • ThreadLocal內(nèi)存泄露問題你知道嗎?
  • ThreadLocal中最后為什么要加remove方法?

是什么? 能干嘛

ThreadLocal提供線程局部變量。這些變量與正常的變量不同,因?yàn)槊恳粋€(gè)線程在訪問ThreadLocal實(shí)例的時(shí)候(通過其get或set方法)都有自己的、獨(dú)立初始化的變量副本。ThreadLocal實(shí)例通常是類中的私有靜態(tài)字段,使用它的目的是希望將狀態(tài)(例如,用戶ID或事務(wù)ID)與線程關(guān)聯(lián)起來。

主要解決了讓每個(gè)線程綁定自己的值,通過使用get()和set()方法,獲取默認(rèn)值或?qū)⑵渲蹈臑楫?dāng)前線程所存的副本的值從而避免了線程安全問題。

一句話如何才能不爭(zhēng)搶

  1. 加入synchronized或者Lock控制資源的訪問順序
  2. 人手一份,大家各自安好,沒必要搶奪

舉個(gè)栗子-阿里規(guī)范

為什么SimpleDateFormat是線程不安全的?

官方文檔說明

測(cè)試

public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    /**
     * 模擬并發(fā)環(huán)境下使用SimpleDateFormat的parse方法將字符串轉(zhuǎn)換成Date對(duì)象
     * @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)部有一個(gè)Calendar對(duì)象引用,它用來儲(chǔ)存和這個(gè)SimpleDateFormat相關(guān)的日期信息。

例如sdf.parse(dateStr),sdf.format(date) 諸如此類的方法參數(shù)傳入的日期相關(guān)String,Date等等, 都是交由Calendar引用來儲(chǔ)存的。這樣就會(huì)導(dǎo)致一個(gè)問題:如果你的SimpleDateFormat是個(gè)static的, 那么多個(gè)thread 之間就會(huì)共享這個(gè)SimpleDateFormat, 同時(shí)也是共享這個(gè)Calendar引用。

SimpleDataFormat的parse()方法會(huì)先調(diào)用Calendar.clear(),然后調(diào)用Calendar.add(),如果一個(gè)線程先調(diào)用了add()然后另一個(gè)線程又調(diào)用了clear(),這時(shí)候parse()方法解析的時(shí)間就不對(duì)了。

舉個(gè)例子:

假設(shè)線程 A 剛執(zhí)行完 calendar.setTime(date) 語句,把時(shí)間設(shè)置為 2020-09-01,但線程還沒執(zhí)行完,線程 B 又執(zhí)行了 calendar.setTime(date) 語句,把時(shí)間設(shè)置為 2020-09-02,這個(gè)時(shí)候就出現(xiàn)幻讀了,線程 A 繼續(xù)執(zhí)行下去的時(shí)候,拿到的 calendar.getTime 得到的時(shí)間就是線程B改過之后的。

如何解決

  • 解決1

將SimpleDateFormat定義成局部變量。

缺點(diǎn):每調(diào)用一次方法就會(huì)創(chuàng)建一個(gè)SimpleDateFormat對(duì)象,方法結(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,也叫做線程本地變量或者線程本地存儲(chǔ)

private static final ThreadLocal  sdf_threadLocal =
            ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    /**
     * ThreadLocal可以確保每個(gè)線程都可以得到各自單獨(dú)的一個(gè)SimpleDateFormat的對(duì)象,那么自然也就不存在競(jìng)爭(zhēng)問題了。
     * @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方時(shí)間庫

ThreadLocal Thread ThreadLocalMap 之間的關(guān)系

ThreadLocal :每個(gè)線程通過此對(duì)象都會(huì)返回各自的值,互不干擾,這是因?yàn)槊總€(gè)線程都存著自己的一份副本。需要注意的是線程結(jié)束后,它所保存的所有副本都將進(jìn)行垃圾回收(除非存在對(duì)這些副本的其他引用)

ThreadLocal的get操作是這樣執(zhí)行的:ThreadLocalMap map = thread.threadLocals -> return map.getEntry(threadLocal)ThreadLocal的set操作是這樣執(zhí)行的:ThreadLocalMap map = thread.threadLocals -> map.set(threadLocal, value)

三者的關(guān)系是:

  • 每個(gè)Thread對(duì)應(yīng)的所有ThreadLocal副本都存放在ThreadLocalMap對(duì)象中,key是ThreadLocal,value是副本數(shù)據(jù)。
  • ThreadLocalMap對(duì)象存放在Thread對(duì)象中。
  • 通過ThreadLocal獲取副本數(shù)據(jù)時(shí),實(shí)際是通過訪問Thread來獲取ThreadLocalMap,再通過ThreadLocalMap獲取副本數(shù)據(jù)。

ThreadLocal內(nèi)存泄露問題

阿里手冊(cè)

什么是內(nèi)存泄漏

不再會(huì)被使用的對(duì)象或者變量占用的內(nèi)存不能被回收,就是內(nèi)存泄露。

ThreadLocal在保存的時(shí)候會(huì)把自己當(dāng)做Key存在ThreadLocalMap中,正常情況應(yīng)該是key和value都應(yīng)該被外界強(qiáng)引用才對(duì),但是現(xiàn)在key被設(shè)計(jì)成WeakReference弱引用了。

強(qiáng)引用、軟引用、虛引用、弱引用

強(qiáng)引用

當(dāng)內(nèi)存不足,jvm開始垃圾回收,對(duì)于強(qiáng)引用的對(duì)象,就算是出現(xiàn)了OOM也不會(huì)對(duì)該對(duì)象進(jìn)行回收。這也是Java中最常見的普通對(duì)象的引用,只要還有強(qiáng)引用指向這個(gè)對(duì)象,就不會(huì)被垃圾回收。當(dāng)這個(gè)對(duì)象沒有了其他的引用關(guān)系,只要是超過了引用的作用域,或者顯示的將強(qiáng)引用賦值為null,一般就可以進(jìn)行垃圾回收了。

軟引用

軟引用是相對(duì)強(qiáng)引用弱化了一些的引用,對(duì)于軟引用的對(duì)象來說:

  • 當(dāng)內(nèi)存充足時(shí),它不會(huì)被回收。
  • 當(dāng)內(nèi)存不足時(shí)。會(huì)被回收。

通常用在對(duì)內(nèi)存敏感的程序中,就像高速緩存。

弱引用

發(fā)現(xiàn)即回收

弱引用也是用來描述那些非必需對(duì)象,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生為止。在系統(tǒng)GC時(shí),只要發(fā)現(xiàn)弱引用,不管系統(tǒng)堆空間使用是否充足,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。

但是,由于垃圾回收器的線程通常優(yōu)先級(jí)很低,因此,并不一定能很快地發(fā)現(xiàn)持有弱引用的對(duì)象。在這種情況下,弱引用對(duì)象可以存在較長(zhǎng)的時(shí)間。弱引用和軟引用一樣,在構(gòu)造弱引用時(shí),也可以指定一個(gè)引用隊(duì)列,當(dāng)弱引用對(duì)象被回收時(shí),就會(huì)加入指定的引用隊(duì)列,通過這個(gè)隊(duì)列可以跟蹤對(duì)象的回收情況。軟引用、弱引用都非常適合來保存那些可有可無的緩存數(shù)據(jù)。如果這么做,當(dāng)系統(tǒng)內(nèi)存不足時(shí),這些緩存數(shù)據(jù)會(huì)被回收,不會(huì)導(dǎo)致內(nèi)存溢出。而當(dāng)內(nèi)存資源充足時(shí),這些緩存數(shù)據(jù)又可以存在相當(dāng)長(zhǎng)的時(shí)間,從而起到加速系統(tǒng)的作用。在JDK1.2版之后提供了WeakReference類來實(shí)現(xiàn)弱引用

// 聲明強(qiáng)引用
Object obj = new Object();
// 創(chuàng)建一個(gè)弱引用
WeakReference<Object> sf = new WeakReference<>(obj);
obj = null; //銷毀強(qiáng)引用,這是必須的,不然會(huì)存在強(qiáng)引用和弱引用

虛引用

也稱為“幽靈引用”或者“幻影引用”,是所有引用類型中最弱的一個(gè)

一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)決定對(duì)象的生命周期。如果一個(gè)對(duì)象僅持有虛引用,那么它和沒有引用幾乎是一樣的,隨時(shí)都可能被垃圾回收器回收。它不能單獨(dú)使用,也無法通過虛引用來獲取被引用的對(duì)象。當(dāng)試圖通過虛引用的get()方法取得對(duì)象時(shí),總是null。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的在于跟蹤垃圾回收過程。比如:能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。

虛引用必須和引用隊(duì)列一起使用。虛引用在創(chuàng)建時(shí)必須提供一個(gè)引用隊(duì)列作為參數(shù)。當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對(duì)象時(shí),如果發(fā)現(xiàn)它還有虛引用,就會(huì)在回收對(duì)象后,將這個(gè)虛引用加入引用隊(duì)列,以通知應(yīng)用程序?qū)ο蟮幕厥涨闆r。由于虛引用可以跟蹤對(duì)象的回收時(shí)間,因此,也可以將一些資源釋放操作放置在虛引用中執(zhí)行和記錄。在JDK1.2版之后提供了PhantomReference類來實(shí)現(xiàn)虛引用。

// 聲明強(qiáng)引用
Object obj = new Object();
// 聲明引用隊(duì)列
ReferenceQueue phantomQueue = new ReferenceQueue();
// 聲明虛引用(還需要傳入引用隊(duì)列)
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null;

內(nèi)存泄漏問題

回到ThreadLocal這邊來說:ThreadLocal在沒有外部強(qiáng)引用時(shí),發(fā)生GC時(shí)會(huì)被回收,如果創(chuàng)建ThreadLocal的線程一直持續(xù)運(yùn)行,那么這個(gè)Entry對(duì)象中的value就有可能一直得不到回收,發(fā)生內(nèi)存泄露。

就比如線程池里面的線程,線程都是復(fù)用的,那么之前的線程實(shí)例處理完之后,出于復(fù)用的目的線程依然存活,所以,ThreadLocal設(shè)定的value值被持有,導(dǎo)致內(nèi)存泄露。

按照道理一個(gè)線程使用完,ThreadLocalMap是應(yīng)該要被清空的,但是現(xiàn)在線程被復(fù)用了。

那怎么解決?

在代碼的最后使用remove就好了,我們只要記得在使用的最后用remove把值清空就好了。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("張三");
    ……
} finally {
    localName.remove();
}

remove的源碼很簡(jiǎn)單,找到對(duì)應(yīng)的值全部置空,這樣在垃圾回收器回收的時(shí)候,會(huì)自動(dòng)把他們回收掉。

那為什么ThreadLocalMap的key要設(shè)計(jì)成弱引用?

key不設(shè)置成弱引用的話就會(huì)造成和entry中value一樣內(nèi)存泄漏的場(chǎng)景。

補(bǔ)充一點(diǎn):ThreadLocal的不足,我覺得可以通過看看netty的fastThreadLocal來彌補(bǔ),大家有興趣可以康康。

key為null的entry的值傳遞的bug

ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個(gè)ThreadLocal沒有外部強(qiáng)引用引用他,那么系統(tǒng)gc的時(shí)候,這個(gè)ThreadLocal勢(shì)必會(huì)被回收,這樣一來,ThreadLocalMap中就會(huì)出現(xiàn)key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話(比如正好用在線程池),這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈。

雖然弱引用,保證了key指向的ThreadLocal對(duì)象能被及時(shí)回收,但是v指向的value對(duì)象是需要ThreadLocalMap調(diào)用get、set時(shí)發(fā)現(xiàn)key為null時(shí)才會(huì)去回收整個(gè)entry、value,因此弱引用不能100%保證內(nèi)存不泄露。我們要在不使用某個(gè)ThreadLocal對(duì)象后,手動(dòng)調(diào)用remoev方法來刪除它,

另外在線程池中,不僅僅是內(nèi)存泄露的問題,因?yàn)榫€程池中的線程是重復(fù)使用的,意味著這個(gè)線程的ThreadLocalMap對(duì)象也是重復(fù)使用的,如果我們不手動(dòng)調(diào)用remove方法,那么后面的線程就有可能獲取到上個(gè)線程遺留下來的value值,造成bug。--值傳遞的問題

值傳遞的問題

ThreadLocal 無法共享線程中的數(shù)據(jù),InheritableThreadLocal 可以再父子線程之間完成值的傳遞

InheritableThreadLocal中createMap,以及getMap方法處理的對(duì)象不一樣了,其中在ThreadLocal中處理的是threadLocals,而InheritableThreadLocal中的是inheritableThreadLocals。Inheritablethreadlocal在父子線程傳遞的時(shí)候,依托的是thread的,在執(zhí)行init( )初始化方法。在init方法中會(huì)執(zhí)行parent.inheritableThreadLocals,這個(gè)方法會(huì)把父線程的值賦給子線程,如果我們使用的是線程池,這樣不會(huì)再重新執(zhí)行init()初始化方法,而是直接使用已經(jīng)創(chuàng)建過的線程,所以這里的值不會(huì)二次產(chǎn)生變化,做不到真正的父子線程數(shù)據(jù)傳遞,就會(huì)出現(xiàn)數(shù)據(jù)錯(cuò)亂的問題。

TransmittableThreadLocal 的原理:它底層使用的TtlRunnable, 在TtlRunnable 構(gòu)造方法中,會(huì)獲取當(dāng)前線程中所有的上下文,并儲(chǔ)存在 AtomicReference 中。而且TtlRunnable 是實(shí)現(xiàn)于 Runnable,在 TtlRunnable run 方法中會(huì)執(zhí)行 Runnable run 方法。當(dāng)線程執(zhí)行時(shí),TtlRunnable 會(huì)從 AtomicReference 中獲取出調(diào)用線程中所有的上下文,并把上下文通過 TransmittableThreadLocal.Transmitter.replay 方法把上下文復(fù)制到當(dāng)前線程。這樣就可以在使用線程池的情況下也可以完成值的傳遞了。

ThreadLocalMap的原理

介紹

ThreadLocalMap 是一個(gè)自定義map,它并沒有實(shí)現(xiàn)Map接口,而且他的Entry是繼承WeakReference(弱引用)的,也沒有看到HashMap中的next,所以不存在鏈表了。

為什么底層用數(shù)組?沒有了鏈表怎么解決Hash沖突呢?

用數(shù)組是因?yàn)椋覀冮_發(fā)過程中可以一個(gè)線程可以有多個(gè)TreadLocal來存放不同類型的對(duì)象的,但是他們都將放到你當(dāng)前線程的ThreadLocalMap里,所以肯定要數(shù)組來存。

從源碼里面看到ThreadLocalMap在存儲(chǔ)的時(shí)候會(huì)給每一個(gè)ThreadLocal對(duì)象一個(gè)threadLocalHashCode,在插入過程中,根據(jù)ThreadLocal對(duì)象的hash值,定位到table中的位置i,

int i = key.threadLocalHashCode & (len-1)。然后會(huì)判斷一下:如果當(dāng)前位置是空的,就初始化一個(gè)Entry對(duì)象放在位置i上;如果位置i不為空,如果這個(gè)Entry對(duì)象的key正好是即將設(shè)置的key,那么就刷新Entry中的value;如果位置i的不為空,而且key不等于entry,那就找下一個(gè)空位置,直到為空為止。

小總結(jié)

  • ThreadLocal 并不解決線程間共享數(shù)據(jù)的問題
  • ThreadLocal 適用于變量在線程間隔離且在方法間共享的場(chǎng)景
  • ThreadLocal 通過隱式的在不同線程內(nèi)創(chuàng)建獨(dú)立實(shí)例副本避免了實(shí)例線程安全的問題
  • 每個(gè)線程持有一個(gè)只屬于自己的專屬M(fèi)ap并維護(hù)了ThreadLocal對(duì)象與具體實(shí)例的映射,該Map由于只被持有它的線程訪問,故不存在線程安全以及鎖的問題
  • ThreadLocalMap的Entry對(duì)ThreadLocal的引用為弱引用,避免了ThreadLocal對(duì)象無法被回收的問題
責(zé)任編輯:武曉燕 來源: 今日頭條
相關(guān)推薦

2020-07-28 08:59:22

JavahreadLocal面試

2025-03-28 08:53:51

2021-02-23 12:43:39

Redis面試題緩存

2023-02-28 11:27:50

線程處理解決共享變量

2019-12-26 09:52:33

Redis集群線程

2025-01-14 11:21:35

2015-09-09 08:45:49

JavaThreadLocal

2023-10-07 08:40:57

緩存屬性Spring

2022-05-11 07:36:12

Java線程安全

2021-01-22 11:58:30

MySQL數(shù)據(jù)庫開發(fā)

2024-10-28 08:15:32

2022-07-26 08:07:03

Python淺拷貝深拷貝

2021-08-05 05:04:50

熱部署模型字節(jié)

2024-09-24 10:28:22

2024-11-11 10:40:19

Java變量副本

2022-10-25 10:20:31

線程變量原理

2018-04-09 08:17:36

線程ThreadLocal數(shù)據(jù)

2022-08-22 18:57:29

React前端面試

2025-04-01 08:25:00

OSPF網(wǎng)絡(luò)IT

2019-11-26 10:30:11

CSS前端面試題
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)