ThreadLocal詳解

文章結(jié)構(gòu)如下:


ThreadLocal思維導(dǎo)圖

簡介

ThreadLocal是為了解決線程安全而產(chǎn)生的航唆。它解決線程安全的思路不同于synchronized:使多個線程對于共享資源的訪問串行化,只有一個線程能夠獲取到對象鎖院刁,其他線程進入同步隊列等待糯钙。也不同于volatile,通過lock指令生成內(nèi)存屏障來使得其他線程訪問變量時需要從主內(nèi)存加載最新值退腥,在線程寫入值時能夠立刻刷新到主內(nèi)存任岸,但是volatile不能保證原子性,因此使用時具有一定局限阅虫。ThreadLocal的解決思路是線程封閉演闭,那么無論線程什么時候,在哪個方法里面訪問ThreadLocal變量颓帝,都只會訪問到自己線程的ThreadLocal值米碰。避免了將參數(shù)通過方法進行傳遞,也無需擔(dān)心其他線程會訪問到本線程的變量值购城。

源碼理解

我們經(jīng)常使用的api是ThreadLocal的get()吕座,set(),remove()方法瘪板,通過get方法切入吴趴,可以發(fā)現(xiàn)對于每個Java線程,都維護了一個ThreadLocalMap侮攀。

    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();
    }

ThreadLocalMap

想要讀懂源碼锣枝,就繞不開對于ThreadLocalMap的理解,ThreadLocalMap本質(zhì)結(jié)構(gòu)跟HashMap差不多兰英,只不過Entry的組成不同撇叁,解決沖突的方式不同。

  1. 數(shù)據(jù)結(jié)構(gòu)
    通過Thread獲取到ThreadLocalMap畦贸,然后每個ThreadLocalMap的Entry存儲著ThreadLocal對象與value的對象關(guān)系陨闹,當(dāng)設(shè)置了多個ThreadLocal變量時楞捂,對于每一個ThreadLocal對象,會在每一個ThreadLocalMap中保存一份趋厉。


    image.png
       static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
  1. 為什么Entry中的ThreadLocal對象是弱引用寨闹?
    在Java中,定義了四種引用:
  • 強引用:Object obj = new Object()君账,對于這種引用繁堡,除非顯示將obj=null,否則虛擬機不會將其回收
  • 軟引用:用來描述一些還有用乡数,但非必須的對象帖蔓。在系統(tǒng)將要內(nèi)存溢出之前,會把軟引用對象列入回收范圍進行第二次回收瞳脓。
  • 弱引用:引用強度比軟引用更弱塑娇,只能生存到下一次虛擬機垃圾回收之前。
  • 虛引用


    image.png

    我們在代碼中實例化的對象引用是保存在虛擬機棧上劫侧,和Entry的key引用同一個對象埋酬,之所以要將Entry的key設(shè)置為弱引用的原因就是如果我們將外部引用設(shè)置為null,那么ThreadLocal的實例不再有強引用烧栋,只有弱引用写妥,在下次虛擬機進行垃圾回收時就可以將其回收了。但是依然還存在內(nèi)存泄漏問題审姓,因為Entry不會被回收珍特。

  1. ThreadLocalMap解決沖突的方式
    解決Hash沖突的方式主要有拉鏈法、開放地址法魔吐、二次hash法扎筒、建立公共溢出區(qū)。ThreadLocalMap解決沖突的方式是開放地址法酬姆,如果通過Hash函數(shù)算出下標(biāo)已經(jīng)存儲過Entry了嗜桌,它會線性環(huán)形搜索沒有被使用的位置。我理解使用線性檢測的原因是只要線程中的ThreadLocal對象不多辞色,那么根據(jù)擴容因子算出的ThreadLocalMap的數(shù)組大小也不會很大骨宠,所以即使退化到最差的o(n),對性能的影響也不大相满,而且這種線性搜索相比鏈?zhǔn)椒绞蕉愿庸?jié)省空間层亿。
    對于線性探測的結(jié)點增刪、擴容可以參考:線性探測解決Hash沖突

內(nèi)存泄漏

上面說到ThreadLocal設(shè)置為弱引用是為了防止內(nèi)存泄漏立美,所謂內(nèi)存泄漏就是指堆中已經(jīng)不再使用的對象沒有被回收匿又,造成空間 的浪費,而且積累下去很可能會造成內(nèi)存溢出悯辙。當(dāng)Entry中的key被回收時琳省,整個Entry就沒有用了,但是由于value還持有虛擬機棧上的強引用躲撰,所以不會被回收针贬,這樣就還是會造成內(nèi)存泄漏。但是ThreadLocal中的get()拢蛋、set()桦他、remove()方法都會調(diào)用replaceStaleEntry、cleanSomeSlots谆棱、expungeStaleEntry方法進行回收

  1. 清理方法:cleanSomeSlots


    搜索清除

下標(biāo)i用來控制訪問的范圍快压,如果沒有找到key為null的Entry,那么會遍歷log2(n)垃瞧,i的下標(biāo)環(huán)形遞增蔫劣。如果找到一個key不為null的位置,n會置為len个从,相當(dāng)于是增大了范圍

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}
  1. expungeStaleEntry的清除邏輯
    cleanSomeSlots函數(shù)脉幢,在key為null的結(jié)點進入expungeStaleEntry方法,將當(dāng)前槽位的value和Entry都設(shè)為null嗦锐,并且還繼續(xù)往下環(huán)形搜索嫌松,一直到table[i]為null才退出,搜索過程中奕污,遇到key為null的結(jié)點就進行清除萎羔,如果key不為null,就對結(jié)點進行rehash碳默,rehash的目的就是為了讓結(jié)點離hash函數(shù)的下標(biāo)更近贾陷,這樣查找的時候就不會在線性搜索浪費時間了。remove和get時嘱根,都會調(diào)用該方法進行清理昵宇。
 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;
            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;
        }
  1. replaceStaleEntry方法
    這個方法在set()過程,當(dāng)key為null時調(diào)用儿子。從i開始首先前環(huán)向搜索臟 entry瓦哎,一直到table[i]=null結(jié)束。然后從下標(biāo)i開始向后搜索柔逼,如果有key相同的就覆蓋蒋譬,并和臟entry交換。根據(jù)不同情況愉适,設(shè)置cleanSomeSlots清除節(jié)點的范圍
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
//向前找到第一個key為null的entry
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
////如果在向后環(huán)形查找過程中發(fā)現(xiàn)key相同的entry就覆蓋并且和臟entry進行交換
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

         //如果在查找過程中還未發(fā)現(xiàn)臟entry犯助,那么就以當(dāng)前位置作為cleanSomeSlots
            //的起點
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }
       //如果向前未搜索到臟entry,則在查找過程遇到臟entry的話维咸,后面就以此時這個位置
        //作為起點執(zhí)行cleanSomeSlots
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }
//如果在查找過程中沒有找到可以覆蓋的entry剂买,則將新的entry插入在臟entry
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

對于下面這個例子惠爽,插入的位置是4,從下標(biāo)4向前搜索到3就停止瞬哼,更新slotExpunge為3.再從4向后遍歷尋找可覆蓋的entry婚肆,當(dāng)前例子未找到,于是以slotExpunge為下標(biāo)調(diào)用cleanExpunge清理臟entry坐慰。


image.png

最佳實踐

使用場景

  1. 數(shù)據(jù)庫連接
    Hibernate的數(shù)據(jù)庫連接池就是將connection放進threadlocal實現(xiàn)的
  2. 用戶Session等信息
  3. 對請求的requestBody较性,requestUrl等進行處理
    代碼中ThreadLocal修飾了requestBody變量,因為服務(wù)器使用的是Tomcat结胀,所以一個請求會交給一個線程來處理赞咙,那么requestBody的get()和set方法設(shè)置的就是當(dāng)前線程的請求體的值,跟其他線程互不影響糟港。
public class ReqLogInterceptor implements HandlerInterceptor {
ThreadLocal<String> requestBody = new ThreadLocal<String>();
  @Override
  public boolean preHandle(HttpServletRequest httpServletRequest,
      HttpServletResponse httpServletResponse, Object o) throws Exception {
    requestBody.set("");
    return true;
  }
  @Override
  public void postHandle(HttpServletRequest httpServletRequest,
      HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView)
      throws Exception {
    if (httpServletResponse instanceof ContentCachingResponseWrapper) {
      responseBody.set("");
      byte[] body = ((ContentCachingResponseWrapper) httpServletResponse).getContentAsByteArray();
      responseBody.set(new String(body, httpServletResponse.getCharacterEncoding()));
    }
  }

  }

及時remove

及時調(diào)用ThreadLocal的remove方法攀操,可以避免內(nèi)存泄漏問題,更重要的是防止造成業(yè)務(wù)邏輯的錯亂秸抚,因為通常會使用線程池管理線程崔赌,如果一個用戶登錄之后的name相關(guān)的ThreadLocal對象,沒有及時remove耸别,那么其他用戶登錄進來之后健芭,會發(fā)現(xiàn)自己的用戶名顯示錯誤。

父子線程通信

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末秀姐,一起剝皮案震驚了整個濱河市慈迈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌省有,老刑警劉巖痒留,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蠢沿,居然都是意外死亡伸头,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門舷蟀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恤磷,“玉大人,你說我怎么就攤上這事野宜∩ú剑” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵匈子,是天一觀的道長河胎。 經(jīng)常有香客問我,道長虎敦,這世上最難降的妖魔是什么游岳? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任政敢,我火速辦了婚禮,結(jié)果婚禮上胚迫,老公的妹妹穿的比我還像新娘喷户。我一直安慰自己,他們只是感情好晌区,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著通贞,像睡著了一般朗若。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上昌罩,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天哭懈,我揣著相機與錄音,去河邊找鬼茎用。 笑死遣总,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的轨功。 我是一名探鬼主播旭斥,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼古涧!你這毒婦竟也來了垂券?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤羡滑,失蹤者是張志新(化名)和其女友劉穎菇爪,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柒昏,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡凳宙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了职祷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氏涩。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖有梆,靈堂內(nèi)的尸體忽然破棺而出削葱,到底是詐尸還是另有隱情,我是刑警寧澤淳梦,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布析砸,位于F島的核電站,受9級特大地震影響爆袍,放射性物質(zhì)發(fā)生泄漏首繁。R本人自食惡果不足惜作郭,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望弦疮。 院中可真熱鬧夹攒,春花似錦、人聲如沸胁塞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽啸罢。三九已至编检,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扰才,已是汗流浹背允懂。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留衩匣,地道東北人蕾总。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像琅捏,于是被迫代替她去往敵國和親生百。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348

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

  • 介紹 顧名思義這個類提供線程局部變量每個線程(通過其get或set方法)都有自己獨立初始化的變量副本 Thread...
    Ray昱成閱讀 239評論 0 0
  • 1 ThreadLocal簡介 多線程訪問同一個共享變量的時候容易出現(xiàn)并發(fā)問題柄延,特別是多個線程對一個變量進行寫入的...
    愛健身的兔子閱讀 437評論 0 0
  • ThreadLocal是什么置侍? ThreadLocal是一個關(guān)于創(chuàng)建線程局部變量的類。 通常情況下拦焚,我們創(chuàng)建的變量...
    chenjieping1995閱讀 2,238評論 1 3
  • 來自:https://www.cnblogs.com/fsmly/p/11020641.html 一蜡坊、Thread...
    dinel閱讀 380評論 0 0
  • 1. 概念 ThreadLocal 用于提供線程局部變量,在多線程環(huán)境可以保證各個線程里的變量獨立于其它線程里的變...
    zly394閱讀 1,715評論 0 1