ThreadLocal基礎(chǔ)及少量面試題

什么是ThreadLocal變量

ThreadLoal 變量垫卤,線程局部變量社牲,同一個 ThreadLocal 所包含的對象,在不同的 Thread 中有不同的副本纺铭。這里有幾點需要注意:

  • 因為每個 Thread 內(nèi)有自己的實例副本寇钉,且該副本只能由當前 Thread 使用。這是也是 ThreadLocal 命名的由來舶赔。

  • 既然每個 Thread 有自己的實例副本扫倡,且其它 Thread 不可訪問,那就不存在多線程間共享的問題竟纳。

ThreadLocal 提供了線程本地的實例撵溃。它與普通變量的區(qū)別在于疚鲤,每個使用該變量的線程都會初始化一個完全獨立的實例副本。ThreadLocal 變量通常被private static修飾缘挑。當一個線程結(jié)束時集歇,它所使用的所有 ThreadLocal 相對的實例副本都可被回收∮锾裕總的來說诲宇,ThreadLocal 適用于每個線程需要自己獨立的實例且該實例需要在多個方法中被使用,也即變量在線程間隔離而在方法或類間共享的場景惶翻。

ThreadLocal實現(xiàn)原理

首先 ThreadLocal 是一個泛型類姑蓝,保證可以接受任何類型的對象。因為一個線程內(nèi)可以存在多個 ThreadLocal 對象吕粗,所以其實是 ThreadLocal 內(nèi)部維護了一個 Map 它掂,這個 Map 不是直接使用的 HashMap ,而是 ThreadLocal 實現(xiàn)的一個叫做 ThreadLocalMap 的靜態(tài)內(nèi)部類溯泣。而我們使用的 get()、set() 方法其實都是調(diào)用了這個ThreadLocalMap類對應(yīng)的 get()榕茧、set() 方法垃沦。例如下面的 set 方法:

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

get方法:

public T get() {   
        Thread t = Thread.currentThread();   
        ThreadLocalMap map = getMap(t);   
        if (map != null)   
            return (T)map.get(this);   

        // Maps are constructed lazily.  if the map for this thread   
        // doesn't exist, create it, with this ThreadLocal and its   
        // initial value as its only entry.   
        T value = initialValue();   
        createMap(t, value);   
        return value;   
    }

createMap方法:

void createMap(Thread t, T firstValue) {   
        t.threadLocals = new ThreadLocalMap(this, firstValue);   
    } 

ThreadLocalMap是個靜態(tài)的內(nèi)部類:

 static class ThreadLocalMap {   
    ........   
    }  

最終的變量是放在了當前線程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上用押,ThreadLocal 可以理解為只是ThreadLocalMap的封裝肢簿,傳遞了變量值。

內(nèi)存泄漏問題

實際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用蜻拨,弱引用的特點是池充,如果這個對象只存在弱引用,那么在下一次垃圾回收的時候必然會被清理掉缎讼。所以如果 ThreadLocal 沒有被外部強引用的情況下收夸,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap中使用這個 ThreadLocal 的 key 也會被清理掉血崭。但是卧惜,value 是強引用,不會被清理夹纫,這樣一來就會出現(xiàn) key 為 null 的 value咽瓷。ThreadLocalMap實現(xiàn)中已經(jīng)考慮了這種情況,在調(diào)用 set()舰讹、get()茅姜、remove() 方法的時候,會清理掉 key 為 null 的記錄月匣。如果說會出現(xiàn)內(nèi)存泄漏钻洒,那只有在出現(xiàn)了 key 為 null 的記錄后奋姿,沒有手動調(diào)用 remove() 方法,并且之后也不再調(diào)用 get()航唆、set()卵迂、remove() 方法的情況下掏熬。

使用場景

如上文所述,ThreadLocal 適用于如下兩種場景

  • 每個線程需要有自己單獨的實例

  • 實例需要在多個方法中共享,但不希望被多線程共享

對于第一點呈驶,每個線程擁有自己實例,實現(xiàn)它的方式很多竖共。例如可以在線程內(nèi)部構(gòu)建一個單獨的實例苗胀。ThreadLoca 可以以非常方便的形式滿足該需求。對于第二點享潜,可以在滿足第一點(每個線程有自己的實例)的條件下困鸥,通過方法間引用傳遞的形式實現(xiàn)。ThreadLocal 使得代碼耦合度更低剑按,且實現(xiàn)更優(yōu)雅疾就。擴展:來探討一下最近面試問的ThreadLocal問題

1)存儲用戶Session

一個簡單的用ThreadLocal來存儲Session的例子:

private static final ThreadLocal threadSession = new ThreadLocal();

    public static Session getSession() throws InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }

2)解決線程安全的問題

比如Java7中的SimpleDateFormat不是線程安全的,可以用ThreadLocal來解決這個問題:

public class DateUtil {
    private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String formatDate(Date date) {
        return format1.get().format(date);
    }
}

這里的DateUtil.formatDate()就是線程安全的了艺蝴。(Java8里的 java.time.format.DateTimeFormatter是線程安全的猬腰,Joda time里的DateTimeFormat也是線程安全的)。

來探討一下最近面試問的ThreadLocal問題

中高級階段開發(fā)者出去面試猜敢,應(yīng)該躲不開ThreadLocal相關(guān)問題姑荷,本文就常見問題做出一些解答,歡迎留言探討缩擂。

ThreadLocal為java并發(fā)提供了一個新的思路鼠冕, 它用來存儲Thread的局部變量, 從而達到各個Thread之間的隔離運行胯盯。它被廣泛應(yīng)用于框架之間的用戶資源隔離懈费、事務(wù)隔離等。

但是用不好會導(dǎo)致內(nèi)存泄漏博脑, 本文重點用于對它的使用過程的疑難解答楞捂, 相信仔細閱讀完后的朋友可以隨心所欲的安全使用它。

內(nèi)存泄漏原因探索

ThreadLocal操作不當會引發(fā)內(nèi)存泄露趋厉,最主要的原因在于它的內(nèi)部類ThreadLocalMap中的Entry的設(shè)計寨闹。

Entry繼承了WeakReference<ThreadLocal<?>>,即Entry的key是弱引用君账,所以key'會在垃圾回收的時候被回收掉繁堡, 而key對應(yīng)的value則不會被回收, 這樣會導(dǎo)致一種現(xiàn)象:key為null,value有值椭蹄。

key為空的話value是無效數(shù)據(jù)闻牡,久而久之,value累加就會導(dǎo)致內(nèi)存泄漏绳矩。

static class ThreadLocalMap {
       static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    ...
}

怎么解決這個內(nèi)存泄漏問題

每次使用完ThreadLocal都調(diào)用它的remove()方法清除數(shù)據(jù)罩润。因為它的remove方法會主動將當前的key和value(Entry)進行清除。

private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear(); // 清除key
            expungeStaleEntry(i);  // 清除value
            return;
        }
    }
}

e.clear()用于清除Entry的key翼馆,它調(diào)用的是WeakReference中的方法:this.referent = null

expungeStaleEntry(i)用于清除Entry對應(yīng)的value割以, 這個后面會詳細講。

JDK開發(fā)者是如何避免內(nèi)存泄漏的

ThreadLocal的設(shè)計者也意識到了這一點(內(nèi)存泄漏)应媚, 他們在一些方法中埋了對key=null的value擦除操作严沥。

這里拿ThreadLocal提供的get()方法舉例,它調(diào)用了ThreadLocalMap#getEntry()方法中姜,對key進行了校驗和對null key進行擦除消玄。

private Entry getEntry(ThreadLocal<?> key) {
    // 拿到索引位置
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

如果key為null, 則會調(diào)用getEntryAfterMiss()方法丢胚,在這個方法中翩瓜,如果k == null , 則調(diào)用expungeStaleEntry(i);方法携龟。

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
    }

expungeStaleEntry(i)方法完成了對key=null 的key所對應(yīng)的value進行賦空兔跌, 釋放了空間避免內(nèi)存泄漏。

同時它遍歷下一個key為空的entry骨宠, 并將value賦值為null, 等待下次GC釋放掉其空間相满。

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    // 遍歷下一個key為空的entry层亿, 并將value指向null
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

同理, set()方法最終也是調(diào)用該方法(expungeStaleEntry)立美, 調(diào)用路徑: set(T value)->map.set(this, value)->rehash()->expungeStaleEntries()

remove方法remove()->ThreadLocalMap.remove(this)->expungeStaleEntry(i)

這樣做匿又, 也只能說盡可能避免內(nèi)存泄漏, 但并不會完全解決內(nèi)存泄漏這個問題建蹄。比如極端情況下我們只創(chuàng)建ThreadLocal但不調(diào)用set碌更、get、remove方法等洞慎。所以最能解決問題的辦法就是用完ThreadLocal后手動調(diào)用remove().

手動釋放ThreadLocal遺留存儲?你怎么去設(shè)計/實現(xiàn)痛单?

這里主要是強化一下手動remove的思想和必要性,設(shè)計思想與連接池類似劲腿。

包裝其父類remove方法為靜態(tài)方法旭绒,如果是spring項目, 可以借助于bean的聲明周期, 在攔截器的afterCompletion階段進行調(diào)用挥吵。

弱引用導(dǎo)致內(nèi)存泄漏重父,那為什么key不設(shè)置為強引用

這個問題就比較有深度了,是你談薪的小小資本忽匈。

如果key設(shè)置為強引用房午, 當threadLocal實例釋放后, threadLocal=null丹允, 但是threadLocal會有強引用指向threadLocalMap郭厌,threadLocalMap.Entry又強引用threadLocal, 這樣會導(dǎo)致threadLocal不能正常被GC回收嫌松。

弱引用雖然會引起內(nèi)存泄漏沪曙, 但是也有set、get萎羔、remove方法操作對null key進行擦除的補救措施液走, 方案上略勝一籌。

線程執(zhí)行結(jié)束后會不會自動清空Entry的value

一并考察了你的gc基礎(chǔ)贾陷。

事實上缘眶,當currentThread執(zhí)行結(jié)束后, threadLocalMap變得不可達從而被回收髓废,Entry等也就都被回收了巷懈,但這個環(huán)境就要求不對Thread進行復(fù)用,但是我們項目中經(jīng)常會復(fù)用線程來提高性能慌洪, 所以currentThread一般不會處于終止狀態(tài)顶燕。

Thread和ThreadLocal有什么聯(lián)系呢

ThreadLocal的概念。

Thread和ThreadLocal是綁定的冈爹, ThreadLocal依賴于Thread去執(zhí)行涌攻, Thread將需要隔離的數(shù)據(jù)存放到ThreadLocal(準確的講是ThreadLocalMap)中, 來實現(xiàn)多線程處理。

相關(guān)問題擴展

加分項來了频伤。

spring如何處理bean多線程下的并發(fā)問題

ThreadLocal天生為解決相同變量的訪問沖突問題恳谎, 所以這個對于spring的默認單例bean的多線程訪問是一個完美的解決方案。spring也確實是用了ThreadLocal來處理多線程下相同變量并發(fā)的線程安全問題憋肖。

spring 如何保證數(shù)據(jù)庫事務(wù)在同一個連接下執(zhí)行的

要想實現(xiàn)jdbc事務(wù)因痛, 就必須是在同一個連接對象中操作, 多個連接下事務(wù)就會不可控岸更, 需要借助分布式事務(wù)完成鸵膏。那spring 如何保證數(shù)據(jù)庫事務(wù)在同一個連接下執(zhí)行的呢?

DataSourceTransactionManager 是spring的數(shù)據(jù)源事務(wù)管理器怎炊, 它會在你調(diào)用getConnection()的時候從數(shù)據(jù)庫連接池中獲取一個connection较性, 然后將其與ThreadLocal綁定用僧, 事務(wù)完成后解除綁定。這樣就保證了事務(wù)在同一連接下完成赞咙。

概要源碼:

1.事務(wù)開始階段:org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin->TransactionSynchronizationManager#bindResource->org.springframework.transaction.support.TransactionSynchronizationManager#bindResource

image

2.事務(wù)結(jié)束階段:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion->TransactionSynchronizationManager#unbindResource->org.springframework.transaction.support.TransactionSynchronizationManager#unbindResource->TransactionSynchronizationManager#doUnbindResource

image

參考:
https://mp.weixin.qq.com/s/xMWiRDDLfCy4Vsa2jZBSaA
如有侵權(quán)责循,請聯(lián)系我刪除

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市攀操,隨后出現(xiàn)的幾起案子院仿,更是在濱河造成了極大的恐慌,老刑警劉巖速和,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件歹垫,死亡現(xiàn)場離奇詭異,居然都是意外死亡颠放,警方通過查閱死者的電腦和手機排惨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碰凶,“玉大人暮芭,你說我怎么就攤上這事∮停” “怎么了辕宏?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長砾莱。 經(jīng)常有香客問我瑞筐,道長,這世上最難降的妖魔是什么腊瑟? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任聚假,我火速辦了婚禮,結(jié)果婚禮上闰非,老公的妹妹穿的比我還像新娘膘格。我一直安慰自己,他們只是感情好河胎,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布闯袒。 她就那樣靜靜地躺著虎敦,像睡著了一般游岳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上其徙,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天胚迫,我揣著相機與錄音,去河邊找鬼唾那。 笑死访锻,一個胖子當著我的面吹牛褪尝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播期犬,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼河哑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了龟虎?” 一聲冷哼從身側(cè)響起璃谨,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鲤妥,沒想到半個月后佳吞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡棉安,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年底扳,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贡耽。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡衷模,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出菇爪,到底是詐尸還是另有隱情算芯,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布凳宙,位于F島的核電站熙揍,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏氏涩。R本人自食惡果不足惜届囚,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望是尖。 院中可真熱鬧意系,春花似錦、人聲如沸饺汹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兜辞。三九已至迎瞧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間逸吵,已是汗流浹背凶硅。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扫皱,地道東北人足绅。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓捷绑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親氢妈。 傳聞我的和親對象是個殘疾皇子粹污,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容