ThreadLocal解析

前言

我們都知道ThreadLocal用于為每個線程存儲自己的變量值衅斩,起到線程間隔離的作用船老,那么它到底是怎么運行的呢琉苇,讓我們通過一段demo來進行一下源碼分析匕垫。

    public static void main(String[] args) {

        ThreadLocal<Integer> sThreadLocal = new ThreadLocal<Integer>();
        new Thread(()->{sThreadLocal.set(1);System.out.println("線程1的threadlocal值:"+sThreadLocal.get());}).start();
        new Thread(()->{sThreadLocal.set(2);System.out.println("線程2的threadlocal值:"+sThreadLocal.get());}).start();

    }

輸出結果:

線程1的threadlocal值:1
線程2的threadlocal值:2

源碼解析

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

這里調用了getMap(t)方法,來看一下

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

可以看到返回了當前線程的threadLocals屬性除破,當該屬性不為空時調用其對應的set方法牧氮,否則調用createMap方法進行初始化,首先來看一下createMap方法

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

這里主要做的事情是初始化當前線程的threadLocals瑰枫,來看一下構造方法

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

這里首先創(chuàng)建了一個Entry類型的數(shù)組踱葛,數(shù)組大小為INITIAL_CAPACITY的值16,EntryThreadLocal的一個內(nèi)部類光坝,定義為

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

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

該類繼承了WeakReference尸诽,因此很明顯是一種弱引用的方式,這里其實存在一個潛在的內(nèi)存泄漏問題盯另,那就是key因為弱引用的關系回收了性含,但該Entry對象由于仍可能被ThreadLocalMap對象強引用而無法釋放,這樣該Entry就變成了一個“臟對象”鸳惯,為此代碼里在其他地方對這個問題進行了優(yōu)化商蕴,后面會講到。

i是數(shù)組中的下標芝发,通過當前線程的threadLocalHashCode計算得來绪商,而threadLocalHashCode的計算過程如下:

private final int threadLocalHashCode = nextHashCode();
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

這里的nextHashCode定義如下

    private static AtomicInteger nextHashCode =
        new AtomicInteger();

所以threadLocalHashCode實質是一個以指定步長進行累加的累加器,該步長能較好的將連續(xù)的線程ID散列到2的冪次方的數(shù)組中后德。另外需要說明的是部宿,傳入的Entry的key值是當前ThreadLocal對象,也就是說這個ThreadLocal對象是被弱引用的對象瓢湃,如果沒有別的地方對其進行了強引用理张,一旦觸發(fā)gc該對象就會被回收。

看完createMap方法初始化map后绵患,來看set方法

        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            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)]) {//遍歷Entry不為空的節(jié)點
                ThreadLocal<?> k = e.get();

                if (k == key) { //若該Entry的key為當前的ThreadLocal對象
                    e.value = value;
                    return;
                }

                if (k == null) { //若該ThreadLocal對象已被回收
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);//遍歷到Entry空的節(jié)點則創(chuàng)建
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

由上述代碼看到雾叭,這里主要做的是在一個for循環(huán)中遍歷尋找Entry不為空的節(jié)點,一旦獲取到就填入新的Entry值落蝙,更新數(shù)組size并根據(jù)閾值判斷是否執(zhí)行rehash()方法更新數(shù)組织狐。

而當遍歷到的Entry為非空節(jié)點時,會有以下操作:若該Entry的key為當前的ThreadLocal對象時筏勒,直接賦值value移迫;若當獲取到的Entry為臟對象時,會調用replaceStaleEntry(key, value, i)方法進行清理管行。

清理方法

這里有幾個方法值得我們具體看一下厨埋,首先是cleanSomeSlots(i, sz)

        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);//找到臟entry并清除掉
                }
            } while ( (n >>>= 1) != 0);//通過n控制循環(huán)次數(shù)
            return removed;
        }

該方法用來遍歷清除臟Entry,一旦遍歷過程中發(fā)現(xiàn)了臟Entry捐顷,則會調用expungeStaleEntry(i)方法清除掉荡陷,并且重置n增加遍歷次數(shù)雨效。那么expungeStaleEntry(i)做了什么呢

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

可以看到清除臟Entry的方式其實很簡單,就是將該Entry位置設為null废赞,這樣一來失去了強引用的臟Entry就會被gc回收徽龟。另外可以看到的是,expungeStaleEntry(i)方法清除了i位置的臟Entry后唉地,并不會停下据悔,而是會繼續(xù)遍歷下一個位置清除臟Entry

接著看一下replaceStaleEntry(key, value, i)方法

        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;//向前找到第一個臟Entry

            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;

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

                    //如果在查找過程中還未發(fā)現(xiàn)臟Entry渣蜗,那么就以當前位置作為清除的起點
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }
                   //如果向前未搜索到臟Entry屠尊,而在查找過程遇到臟Entry的話,后面就以此時這個位置作為起點執(zhí)行清除
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // 沒有發(fā)現(xiàn)對應的key耕拷,則在該臟位置創(chuàng)建新Entry
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            //清除剩余臟Entry
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

該方法首先前向搜尋臟Entry記錄為slotToExpunge,接著從staleSlot位置開始后向搜索托享,如果在查找過程中未發(fā)現(xiàn)臟Entry骚烧,且存在當前的key,那么賦值value闰围,并且以當前位置staleSlot作為清除的起點赃绊;若for循環(huán)結束仍未找到對應的key,則在staleSlot位置創(chuàng)建新的Entry節(jié)點羡榴,并從slotToExpunge位置開始清除剩余的臟Entry碧查。

get方法

看完了ThreadLocal的set方法,接著來看看其get方法

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

這里可以看到校仑,首先通過getMap方法獲取當前線程的threadLocals忠售,如果該map不為空,以當前ThreadLocal對象做為key取出對應的Entry得到value值迄沫。若沒有順利取得value值稻扬,則會執(zhí)行setInitialValue()方法,我們來看看該方法做了什么羊瘩。

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

initialValue()方法為value設置了null值泰佳,通過當前線程獲取threadLocals,若map存在則調用set方法尘吗,否則調用createMap方法創(chuàng)建threadLocals逝她。

總結和思考

從以上分析可以了解到,Thread對象持有自己的ThreadLocalMap對象睬捶,該對象實質為一個Entry數(shù)組黔宛,每個Entry是一個鍵值對,key是當前的ThreadLocal對象侧戴,并且對該ThreadLocal對象使用的是弱引用宁昭。這里存在兩個問題:

  1. 為什么采用這種引用結構跌宛;
  2. 這里是否存在內(nèi)存泄漏問題。

對于問題1积仗,由于ThreadLocal的生命周期普遍長于Thread疆拘,因此當Thread生命周期結束以后,即使ThreadLocal仍存在寂曹,但由于弱引用的關系哎迄,ThreadLocalMap就可以被釋放了。

低于問題2隆圆,當ThreadLocal提前于Thread結束生命周期漱挚,比如線程池這種Thread長期不結束的情況,此時ThreadLocal對象僅有來自ThreadLocalMapEntry的弱引用渺氧,因此該ThreadLocal對象時可以被回收掉的旨涝,那么接下來就會出現(xiàn)對應的Entry中key被置為null的情況,那么這個Entry就再也不可能被調用到侣背,就發(fā)生了內(nèi)存泄漏白华。為了處理這種情況,在源碼的set方法中我們看到了大量的臟Entry清理策略贩耐,另外其實在remove方法中也有類似的清理策略弧腥,我們也在使用完ThreadLocal后采用手動調用remove方法的方式來避免內(nèi)存泄漏的情況。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末潮太,一起剝皮案震驚了整個濱河市管搪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌铡买,老刑警劉巖更鲁,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異寻狂,居然都是意外死亡岁经,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門蛇券,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缀壤,“玉大人,你說我怎么就攤上這事纠亚√聊剑” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵蒂胞,是天一觀的道長图呢。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么蛤织? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任赴叹,我火速辦了婚禮,結果婚禮上指蚜,老公的妹妹穿的比我還像新娘乞巧。我一直安慰自己,他們只是感情好摊鸡,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布绽媒。 她就那樣靜靜地躺著,像睡著了一般免猾。 火紅的嫁衣襯著肌膚如雪是辕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天猎提,我揣著相機與錄音获三,去河邊找鬼。 笑死锨苏,一個胖子當著我的面吹牛石窑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蚓炬,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼躺屁!你這毒婦竟也來了肯夏?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤犀暑,失蹤者是張志新(化名)和其女友劉穎驯击,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體耐亏,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡徊都,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了广辰。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片暇矫。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖择吊,靈堂內(nèi)的尸體忽然破棺而出李根,到底是詐尸還是另有隱情,我是刑警寧澤几睛,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布房轿,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏囱持。R本人自食惡果不足惜夯接,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望纷妆。 院中可真熱鬧盔几,春花似錦、人聲如沸凭需。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽粒蜈。三九已至顺献,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間枯怖,已是汗流浹背注整。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留度硝,地道東北人肿轨。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像蕊程,于是被迫代替她去往敵國和親椒袍。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

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

  • 原理 產(chǎn)生線程安全問題的根源在于多線程之間的數(shù)據(jù)共享藻茂。如果沒有數(shù)據(jù)共享驹暑,就沒有多線程并發(fā)安全問題。ThreadLo...
    Java耕耘者閱讀 298評論 0 0
  • 原理 產(chǎn)生線程安全問題的根源在于多線程之間的數(shù)據(jù)共享辨赐。如果沒有數(shù)據(jù)共享优俘,就沒有多線程并發(fā)安全問題。ThreadLo...
    zhong0316閱讀 367評論 0 4
  • 前言 剛看過EventBus和AndroidEventBus的源碼, 發(fā)現(xiàn)里面都有用到ThreadLocal, 那...
    海之韻Baby閱讀 331評論 0 0
  • 前言 在各大公司招聘筆試和面試題題中掀序,都遇到了很多ThreadLocal的問題帆焕,最近博主在面試的時候也被兩次問到過...
    Kevin_ZGJ閱讀 434評論 1 3
  • ThreadLocal和Synchonized都用語解決多線程并發(fā)訪問的叶雹,可以ThreadLocal與Syncho...
    瀟湘夜雨123閱讀 1,551評論 1 10