ThreadLocal使用以及面試題解析

簡述:

在Java并發(fā)中通惫,如果對于某些對象并不需要做共享操作茂翔,而是希望每個線程把對應的對象復制一份到線程內,加上線程天然的隔離性讽膏,這樣可以完美的避免多個線程搶奪操作同一個對象從而報錯檩电。
ThreadLocal就是為了這個場景而產(chǎn)生的。

ThreadLocal VS Synchronized

ThreadLocal和Synchronized都是為了保證多線程場景下的線程安全府树,但是兩者也有著本質的區(qū)別俐末。
ThreadLocal用于處理變量為不共享,其實現(xiàn)原理其實就是將某些對象納入線程中奄侠,這樣對于某個公共的變量卓箫,如果有十個線程需要操作該對象,每個對象都將該變量Copy一份放入線程內垄潮,配合線程天然的隔離性可以避免多個線程搶奪共享變量的問題烹卒。
Synchronized用于處理變量共享導致的線程不安全問題,通過Synchronized鎖可以保證多線程的可見性弯洗、事務一致性旅急、順序性。簡而言之牡整,當你需要安全的處理多線程使用的共享變量時且需要線程之間該變量的互通(而不是簡單的Copy副本各自處理)那么可以使用重量級鎖Synchronized藐吮。

聊一下ThreadLocal實現(xiàn)原理

下圖簡單的反應一下:Thread、ThreadLocalMap、ThredLocal谣辞、Entry


ThreadLocal概貌圖

查看Thread類會發(fā)現(xiàn)迫摔,在Thread類中有一個全局變量:

// ThreadLocalMap是ThreadLocal的一個靜態(tài)內部類
ThreadLocal.ThreadLocalMap threadlocals;

那我們就順著這個思路來聊一下原理,先聊一下ThreadLocalMap對象是如何掛載到線程類并且之后線程是如何獲取對應相關聯(lián)的ThreadLocalMap的泥从。之后再去聊一下ThreadLocalMap內部的處理機制句占。

線程如何和ThreadLocalMap關聯(lián)

在Thread類中有如下源碼:


Thread.class

也就是說在線程內部有一個變量threadLocals。每個線程初始化時躯嫉,該變量的默認值都為null纱烘。
那么ThreadLocalMap是何時以及如何會與Thread線程的threadLocals相關聯(lián)呢?
其實這里也使用的是一種懶漢思想祈餐,也就是說凹炸,在Thread被創(chuàng)建之后,代碼并不會自動的創(chuàng)建ThreadLocalMap對象并與Thread關聯(lián)昼弟,而是在使用到線程中的ThreadLocal時才會去關聯(lián)啤它,比如,我們以threadLocal.set操作為例舱痘,投過源碼分析:

@Slf4j
public class ThreadLocalTest {
    ThreadLocal<Object> threadLocals = new ThreadLocal<>(); // 往當前線程的ThreadLocal掛載
    @Test
    public void t() {
        threadLocals.set("1");
    }
}

在測試類中調用了set方法变骡,斷點跟蹤一下,看看set方法都做了什么操作:

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

對上述set(T value)方法做說明:

  1. 首先獲取當前線程并賦值給變量t
  2. 根據(jù)t變量調用本類的getMap方法用來獲取ThreadLocalMap對象芭逝。繼續(xù)瞅一眼getMap干了啥:
    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

怎么樣塌碌?其實也就是根據(jù)線程t獲取其內部變量threadLocals。

  1. 緊接著就跟了一個if分支旬盯,分兩種情況台妆,當map == null時候則內部調用createMap方法,如果map不為空胖翰,那么就直接調用ThreadLocalMap.set方法進行賦值操作(由于這個地方主要講的是ThreadLocalMap和Thread的掛載問題接剩,因此map.set(this,value)放到下文詳細描述),主要看一下createMap(t,value)是如何創(chuàng)建ThreadLocalMap對象又如何掛載到Thread上的萨咳,看下面createMap源碼:
    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

發(fā)現(xiàn)懊缺,其內部創(chuàng)建出了ThreadLocalMap對象并將其掛載到Thread類上,參數(shù)為ThreadLocal當前對象this和setValue方法中的Value方法(當然是這樣培他,可以設想在第一次調用ThreadLocal.set方法的時候如果ThreadLocalMap為空則創(chuàng)建鹃两,創(chuàng)建完畢一定是需要緊接著存Value)∫荩看到這里我們知道了ThreadMap和Thread是如何掛載的俊扳。
其實我只是拿ThreadLocal.set操作為例,其實同樣ThreadLocal.get操作也同樣先判斷線程中的ThreadLocalMap是否為空猛遍,若不為空則會調用createMap的方式來進行創(chuàng)建馋记。

ThreadLocalMap類

簡述:在看完了ThreadLocalMap如何與Thread進行掛鉤的碎绎,其實背后原理很簡單,就是一個ThreadLocalMap對象被賦值給了Thread中的threadlocals變量抗果。
所以最核心的代碼其實都在ThreadLocal類和ThreadLocalMap類中(ThreadLocalMap為ThreadLocal的一個靜態(tài)內部類)
我們還是以ThreadLocal.set(Object value)為例,來闡述奸晴,我們想要存入ThreadLocal的值是保存在哪的冤馏。

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

這段代碼熟悉吧?這個就是ThreadLocal.set方法寄啼,上文已經(jīng)詳細說明了createMap方法逮光,現(xiàn)在來看一下map.set方法,先瞅一下源碼:

        /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be 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)]) {
                ThreadLocal<?> k = e.get();

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

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

需要注意的是:ThreadLocalMap他內部的本質其實是一個Entry[]數(shù)組墩划,也就是說涕刚,在ThreadLocalMap中其實并沒有使用ConcurrentHashMap等線程安全的相關數(shù)據(jù)結構,而是通過Entry數(shù)組結合Hash(key)&Entry.length的方式進行對Entry數(shù)組讀寫乙帮。
這一點在讀源碼的時候需要注意杜漠。
看一下ThreadLocal中對Entry[]的定義:

ThreadLocalMap下的Enrty數(shù)組定義

在回來看set核心代碼,其他他就是先根據(jù)set的第一個參數(shù):key(屬于ThreadLocal)然后和Entry當前的容量做&操作察净。然后得到i變量并作為Enry的數(shù)組下標訪問到Entry[i]中的Entry對象驾茴。這個Entry對象就是我們最終需要的,Entry的Key為ThreadLocal對象氢卡,Value為我們保存的Value锈至。拿到這個最終的Entry之后我們就可以做相關的get和set操作了。
看了這么多译秦,再去看一下上述文章的createMap方法中new ThreadLocalMap方法峡捡,看看在初始化的時候是如何決定將當前的ThreadLocal放入到Entry[]數(shù)組中的哪個下標的,看源碼:

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        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);
        }

INITIAL_CAPACITY:16
其實這個地方的
threshold:INITIAL_CAPACITY * 2 / 3
INITIAL_CAPACITY是不是很熟悉筑悴,其實和Map數(shù)據(jù)結構中也有類似的字段们拙。
而threshold就類似于Map中的負載因子了。而上述代碼的ThreadLocal.set方法中阁吝,就有在某些場景下調用refresh方法睛竣,因為Entry是以數(shù)組出現(xiàn)的,所以自然而然的想到求摇,這個地方的擴容其實和JDK中的ArrayList動態(tài)擴容是一個樣子了射沟。

面試題

  1. 在ThreadLocalMap中內部類Entry為何對ThreadLocal采用弱引用的方式?
    答:Entry對于ThreadLocal采用弱引用是為了更好的方便ThreadLocal的GC操作与境;
    在如下情況:當不想再去使用ThreadLocal的時候验夯,正常情況下,我們可以將ThreadLocal的外部引用置為null摔刁,這樣可以輔助下次GC的時候回收掉ThreadLocal變量挥转。但是此刻如果對應的Thread一直處于運行狀態(tài),那么ThreadLocal存在于這樣的一條強引用鏈:Thread -> ThreadLocalMap -> Entry -> ThreadLocal。因此對于ThreadLocal的兩條強引用鏈中只要有一方?jīng)]有斷開绑谣,那么GC在多次也無法對ThreadLocal進行回收党窜。在這樣的情況下,在Entry中使用對ThreadLocal的弱引用借宵,只要Java程序中將ThreadLocal引用置為null幌衣,那么該ThreadLocal將不再存在強引用關系,下次GC可以對ThreadLocal對象進行回收壤玫。

  2. ThreadLocalMap中的Entry對ThreadLocal采用了弱引用的方式方便GC豁护,那為何還會出現(xiàn)內存泄漏的問題?是什么對象可能發(fā)生泄漏欲间?如何解決的楚里?
    答:在思考了第一個問題之后,會發(fā)現(xiàn)猎贴,ThreadLocal確實更加容易回收了班缎,比如只要發(fā)生GC且用戶程序中也沒有對ThreadLocal進行強引用,那么ThreadLocal對象便會被回收她渴。
    但問題是:在ThreadLocal被回收之后吝梅,Entry中就會存在這樣的一對數(shù)據(jù)<null,value>,又因為存在如下強引用鏈惹骂,導致GC時Value無法被回收:Thread->ThreadLocalMap->Entry->Value苏携;直到Thread線程終止。
    此時在編程上如果不加以特殊處理对粪,那么這樣的value值將永遠無法被回收右冻。ThreadLocal中采用的方法是:在set、get著拭、remove方法中每一次操作都會手動將Entry中key為null的value也置為null纱扭,方便在下一次GC的時候進行回收。
    所以在釋放ThreadLocal對象之前儡遮,最好先調用一次remove將value先清空掉乳蛾,否則先釋放了ThreadLocal對象則無法再調用ThreadLocal中的任何方法了。
    如下代碼:

try {
 // 業(yè)務代碼
} finally {
 threadLocal.remove();
 threadLocal = null;
}
  1. 在使用ThreadLocal過程中鄙币,在當前線程下創(chuàng)建子線程肃叶,子線程無法獲取父線程的數(shù)據(jù),如何解決十嘿?
    答:因為子線程對象和父線程對象肯定不是同一個因惭,在ThreadLocal中根據(jù)Thread對象獲取到的Entry對象自然也就不同。
    可以使用ThreadLocal的子類:InheritableThreadLocal绩衷;當一個線程進行創(chuàng)建子線程的過程中蹦魔,父線程會將自身的InheritableThreadLocal變量中的數(shù)據(jù)全部傳遞給子線程的InheritableThreadLocal激率。因此子線程也可以使用父線程ThreadLocal中的數(shù)據(jù)了。見如下代碼:
    private static ThreadLocal local = new ThreadLocal();
    private static InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal();

    public static void main(String[] args) throws InterruptedException {
        local.set("ThreadLocal");
        inheritableThreadLocal.set("InheritableThreadLocal");
        new Thread(() -> {
            System.out.println(local.get());
            System.out.println(inheritableThreadLocal.get());
        }).start();
        Thread.sleep(20000);
    }

  // 結果:
  null
  InheritableThreadLocal
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末勿决,一起剝皮案震驚了整個濱河市乒躺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌低缩,老刑警劉巖嘉冒,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異表制,居然都是意外死亡,警方通過查閱死者的電腦和手機控乾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門么介,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蜕衡,你說我怎么就攤上這事壤短。” “怎么了慨仿?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵久脯,是天一觀的道長。 經(jīng)常有香客問我镰吆,道長帘撰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任万皿,我火速辦了婚禮摧找,結果婚禮上,老公的妹妹穿的比我還像新娘牢硅。我一直安慰自己蹬耘,他們只是感情好,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布减余。 她就那樣靜靜地躺著综苔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪位岔。 梳的紋絲不亂的頭發(fā)上如筛,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天,我揣著相機與錄音抒抬,去河邊找鬼妙黍。 笑死,一個胖子當著我的面吹牛瞧剖,可吹牛的內容都是我干的拭嫁。 我是一名探鬼主播可免,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼做粤!你這毒婦竟也來了浇借?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤怕品,失蹤者是張志新(化名)和其女友劉穎妇垢,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肉康,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡闯估,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了吼和。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涨薪。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖炫乓,靈堂內的尸體忽然破棺而出刚夺,到底是詐尸還是另有隱情,我是刑警寧澤末捣,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布侠姑,位于F島的核電站,受9級特大地震影響箩做,放射性物質發(fā)生泄漏莽红。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一邦邦、第九天 我趴在偏房一處隱蔽的房頂上張望船老。 院中可真熱鬧,春花似錦圃酵、人聲如沸柳畔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽薪韩。三九已至,卻和暖如春捌锭,著一層夾襖步出監(jiān)牢的瞬間俘陷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工观谦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拉盾,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓豁状,卻偏偏與公主長得像捉偏,于是被迫代替她去往敵國和親倒得。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359