死磕Java源碼之ThreadLocal實現(xiàn)分析

死磕Java源碼之ThreadLocal實現(xiàn)分析

通俗的講, ThreadLocal是Java里一種特殊的變量蜘腌。每個線程都有一個ThreadLocalMap沫屡,用來存放ThreadLocal變量表,當(dāng)然這里不是直接通過Map的方式存儲撮珠,而是通過一個table和Entry結(jié)構(gòu)存儲

因為ThreadLocalMap變量是跟線程綁定的沮脖,所以不存在多線程共享變量之間的并發(fā)問題,所以ThreadLocal也就是線程安全的變量芯急。

屏幕快照 2018-09-06 上午11.20.55.png

具體的結(jié)構(gòu)倘潜,在源碼部分說明

ThreadLocal的使用

ThreadLocal主要有以下幾個方法:

public T get() { } // 用來獲取ThreadLocal在當(dāng)前線程中保存的變量副本
public void set(T value) { } //set()用來設(shè)置當(dāng)前線程中變量的副本
public void remove() { } //remove()用來移除當(dāng)前線程中變量的副本
protected T initialValue() { } //initialValue()是一個protected方法,一般是用來在使用時進行重寫的

寫一個demo志于,在main線程和新建線程中涮因,對同一個ThreadLocal變量進行修改,看下修改后的結(jié)果:

public class ThreadLocalDemo {

    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {


        ThreadLocalDemo.threadLocal.set("hello world main");
        System.out.println(ThreadLocalDemo.threadLocal.get());


        try {
            Thread thread = new Thread() {
                public void run() {
                    ThreadLocalDemo.threadLocal.set("hello world thread");
                    System.out.println(ThreadLocalDemo.threadLocal.get());
                };
            };
            thread.start();
            thread.join();
        } catch (Exception ex) {
            System.out.println(ex);
        }

        System.out.println(ThreadLocalDemo.threadLocal.get());

    }

}

執(zhí)行輸出:

hello world main
hello world thread
hello world main

不難看出伺绽,我們在new Thread()中對ThreadLocal的變量threadLocal進行修改后养泡,在main線程中再次輸出嗜湃,其值并沒有收到影響,他們修改的分別是各自的副本澜掩,不會對其他副本有影響购披。

當(dāng)然這里完整的邏輯是應(yīng)該在使用完調(diào)用remove方法刪除threadLocal副本,以防內(nèi)存泄露肩榕。

具體原理見下文

ThreadLocal的內(nèi)存泄漏與源碼分析

ThreadLocal的結(jié)構(gòu)如圖:

image

ThreadLocal為什么會內(nèi)存泄漏

每個Thread 維護一個 ThreadLocalMap 映射表刚陡,這個映射表的 keyThreadLocal實例本身,value 是真正需要存儲的 Object株汉。

也就是說 ThreadLocal 本身并不存儲值筐乳,它只是作為一個 key 來讓線程從 ThreadLocalMap 獲取 value。值得注意的是圖中的虛線乔妈,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作為 Key 的蝙云,弱引用的對象在 GC 時會被回收。

ThreadLocalMap使用ThreadLocal的弱引用作為key路召,如果一個ThreadLocal沒有外部強引用來引用它勃刨,那么系統(tǒng) GC 的時候,這個ThreadLocal勢必會被回收股淡,這樣一來身隐,ThreadLocalMap中就會出現(xiàn)keynullEntry,就沒有辦法訪問這些keynullEntryvalue唯灵,如果當(dāng)前線程再遲遲不結(jié)束的話贾铝,這些keynullEntryvalue就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收,造成內(nèi)存泄漏早敬。

其實忌傻,ThreadLocalMap的設(shè)計中已經(jīng)考慮到這種情況大脉,也加上了一些防護措施:在ThreadLocalget(),set(),remove()的時候都會清除線程ThreadLocalMap里所有keynullvalue搞监,當(dāng)然這只是一種防護措施,最好的使用方法是在用完了ThreadLocal變量時镰矿,調(diào)用remove()方法主動將ThreadLocal和value釋放琐驴。

關(guān)于為什么ThreadLocalMap使用ThreadLocal的弱引用,這就跟弱引用的機制有關(guān)秤标,若引用的對象在JVM執(zhí)行GC的時候就會被回收掉。通過gc前后查看table中對應(yīng)entry對象的referent即可查看是否被回收(示例的前提是ThreadLocal的強引用對象已經(jīng)釋放):

GC之前:

![屏幕快照 2018-09-05 下午5.03.21.png](https://upload-images.jianshu.io/upload_images/1272254-4a2814486c210166.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

GC之后:

此時Entry的referent=null,當(dāng)再次通過調(diào)用get轨功、set曼月、remove方法是,ThreadLocal會有各自的機制衙猪,將Map中key(referent)為空的Entry移除馍乙,并釋放其中的value布近,一定成都避免了內(nèi)存泄漏,此機制源碼分析階段說明丝格。

ThreadLocal源碼分析

ThreadLocal中的關(guān)鍵屬性

//創(chuàng)建ThreadLocal時撑瞧,復(fù)制的HashCode
//HashCode是在全局靜態(tài)的nextHashCode基礎(chǔ)上增加一個HASH_INCREMENT而來
private final int threadLocalHashCode = nextHashCode();

下面分析幾個具體的函數(shù):

  • get方法的實現(xiàn)
/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
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();
}

首先是當(dāng)前線程,然后通過getMap(t)方法獲取到一個map显蝌,map的類型為ThreadLocalMap预伺。然后接著下面獲取到<key,value>鍵值對,注意這里獲取鍵值對傳進去的是 this曼尊,而不是當(dāng)前線程t酬诀。如果獲取成功,則返回value值涩禀。如果map為空料滥,則調(diào)用setInitialValue方法返回value。

  • setInitialValue方法的實現(xiàn)
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;
}
/**
* 構(gòu)造ThreadLocalMap
**/
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);
}

首先是通過調(diào)用initialValue艾船,initialValue是protected方法葵腹,初始化ThreadLocal時可以重寫此函數(shù),相當(dāng)于延遲加載屿岂,然后通過getMap創(chuàng)建threadLocals践宴,如果threadLocals不存在時,會調(diào)用createMap創(chuàng)建一個初始大小為16的Entry數(shù)組table爷怀,并新建一個Entry存入table中阻肩。這個threadLocals就是用來存儲實際的變量副本的,鍵值為當(dāng)前ThreadLocal變量运授,value為變量副本(即T類型的變量)

這里重點看下Entry類

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

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

Entry類是集成自WeakReference烤惊,然后使用ThreadLocal作為了鍵,也就是說這里的ThreadLocal是一個弱引用在GC的時候會被回收吁朦。

接上文柒室,如果map存在,則會調(diào)用map的getEntry方法逗宜,getEntry方法實現(xiàn):

private Entry getEntry(ThreadLocal<?> key) {
    //通過hash算出數(shù)組下標(biāo)
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        //如果取出Entry雄右,并且e.get也就是referent與threadLocal相同,則說明是需要的值纺讲,返回Entry對象e 擂仍,判斷e.get() = key 是解決hash碰撞的情況
        return e;
    else
        //如果下標(biāo)i的Entry不存在或者 其threadLocal不相同,則執(zhí)行此
        return getEntryAfterMiss(key, i, e);
}


private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    
    while (e != null) {
        //說明有此entry熬甚,可能是hash碰撞的結(jié)果
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            //處理已無引用的ThreadLocal變量等逢渔,解決內(nèi)存泄漏的機制之一
            expungeStaleEntry(i);
        else
            //下標(biāo)+1 
            i = nextIndex(i, len);
        e = tab[i];
    }
    //如果getEntry中獲取的entry=null,則說明無此ThreadLocal變量乡括,返回null
    return null;
}

expungeStaleEntry 方法

//刪除可以釋放的Entry
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) {
            //如果發(fā)現(xiàn)ThreadLocal已經(jīng)被釋放掉肃廓,則通過這里來釋放value的引用冲簿,以及刪除數(shù)組table中的Entry
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                //重新設(shè)置Entry在table中的位置
                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;
}

通過對get方法的大致分析,可以分為幾個階段:

1)判斷Map是否存在亿昏,如果不存在初始化Map以及table等

2)如果已存在峦剔,并且獲取到Entry,則返回

3)如果不存在角钩,則調(diào)用expungeStaleEntry清除需要釋放的ThreadLocal吝沫、釋放對value的一用,從table中刪除相應(yīng)下標(biāo)的Entry递礼,以及重新設(shè)置元素在table中的位置

  • set方法的實現(xiàn)
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方法中的createMap與上文的createMap相同惨险,不在做說明,重點看下map.set(this, value); 這里直接在溫中鋒

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) {
           //如果通過hash計算的下標(biāo)取出的entry的key與設(shè)置的相同脊髓,則更新value
            e.value = value;
            return;
        }

        if (k == null) {
            //和HashMap不一樣辫愉,由于Entry key繼承了軟引用,會出現(xiàn)k是null的情況将硝!所以會接著在replaceStaleEntry重新循環(huán)尋找相同的key
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //如果key!= null  并且 k != key 說明存在hash鵬錚
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        //調(diào)用cleanSomeSlots()對table進行清理恭朗,如果沒有任何Entry被清理,并且表的size超過了閾值依疼,就會調(diào)用rehash()方
        rehash();
}

hash散列的鍵值數(shù)據(jù)在存儲過程中可能會發(fā)生碰撞痰腮,大家知道HashMap存儲的是一個Entry鏈,當(dāng)hash發(fā)生沖突后律罢,將新的Entry存放在鏈表最前端膀值。但是ThreadLocalMap不一樣,采用index+1作為重散列的hash值寫入误辑。另外有一點需要注意key出現(xiàn)null的原因是由于Entry的key是繼承了軟引用沧踏,在下一次GC時不管它有沒有被引用都會被回收掉而Value沒有被回收。當(dāng)出現(xiàn)null時巾钉,會調(diào)用replaceStaleEntry()方法接著循環(huán)尋找相同的key翘狱,如果存在,直接替換舊值睛琳。如果不存在盒蟆,則在當(dāng)前位置上重新創(chuàng)建新的Entry.

  • remove方法的實現(xiàn)

  • //ThreadLocal
    public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
         }
    /**
     * ThreadeLocalMap
     * Remove the entry for key.
     */
    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();
                expungeStaleEntry(i);
                return;
            }
        }
    }
    

    remove方法相對簡單踏烙,通過hashcode計算出下標(biāo)师骗,然后判斷key與要刪除的ThreadLocal是否一致,如果一致讨惩,釋放掉相應(yīng)的引用辟癌,并調(diào)用expungeStaleEntry方法清理其他的可以釋放的對象。

ThreadLocal的使用場景

  • 每個線程自己獨享的數(shù)據(jù)荐捻,比如session數(shù)據(jù)
  • 實例需要在多個方法中共享黍少,但不希望被多線程共享

比如在Dubbo中的RpcContext實例寡夹,在RpcContext.java文件中,通過靜態(tài)的ThreadLocal變量厂置,為每個線程持有一個RpcContext對象菩掏,這個RpcContext對象只有在此線程的不同方法中共享使用,在多線程中不會共享昵济,是一種典型的應(yīng)用智绸,包括重寫了initialValue方法

rivate static final ThreadLocal<RpcContext> LOCAL = new ThreadLocal<RpcContext>() {
   @Override
   /**
   * 重新initialValue方法,當(dāng)get時為null時访忿,通過回調(diào)此方法獲取RpcContext實例
   **/
   protected RpcContext initialValue() {
      return new RpcContext();
   }
};

/**
 * get context.
 * 
 * @return context
 */
public static RpcContext getContext() {
    return LOCAL.get();
}

/**
 * remove context.
 * 
 * @see com.alibaba.dubbo.rpc.filter.ContextFilter
 */
public static void removeContext() {
    LOCAL.remove();
}

總結(jié)

  • ThreadLocal 并不解決線程間共享數(shù)據(jù)的問題瞧栗,通過使用ThreadLocal是使數(shù)據(jù)在不同線程有不同的副本,不會有多線程共享數(shù)據(jù)也就不需要解決共享數(shù)據(jù)的問題
  • 每個線程持有一個 Map 并維護了 ThreadLocal 對象與具體實例的映射海铆,該 Map 由于只被持有它的線程訪問迹恐,故不存在線程安全以及鎖的問題
  • ThreadLocalMapEntryThreadLocal 的引用為弱引用,避免了因ThreadLocalMap強引用 ThreadLocal 對象在線程回收之前無法被回收的問題
  • ThreadLocalMap 的 set 方法通過調(diào)用 replaceStaleEntry 方法回收鍵為 null 的 Entry 對象的值(即為具體實例)以及 Entry 對象本身從而防止內(nèi)存泄漏
  • ThreadLocalMap 的 get 方法通過調(diào)用 expungeStaleEntry 方法回收鍵為 null 的 Entry 對象的值(即為具體實例)以及 Entry 對象本身從而防止內(nèi)存泄漏
  • ThreadLocalMap當(dāng)hash發(fā)生沖突后卧斟,并不是與HashMap一樣采用的Entry鏈表將新的Entry存放在鏈表最前端殴边。而是采用index+1作為重散列的hash值來重新存儲Entry值

文章部分自己理解,部分借鑒了大牛們的文章珍语,在此表示感謝找都!如有bug,勞煩指正廊酣!

參考:

https://blog.csdn.net/liulongling/article/details/50607802

http://www.importnew.com/21206.html

http://www.jasongj.com/java/threadlocal/

http://www.importnew.com/22039.html

<https://blog.csdn.net/liulongling/article/details/50607802

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末能耻,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子亡驰,更是在濱河造成了極大的恐慌晓猛,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凡辱,死亡現(xiàn)場離奇詭異戒职,居然都是意外死亡,警方通過查閱死者的電腦和手機透乾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門洪燥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人乳乌,你說我怎么就攤上這事捧韵。” “怎么了汉操?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵再来,是天一觀的道長。 經(jīng)常有香客問我,道長芒篷,這世上最難降的妖魔是什么搜变? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮针炉,結(jié)果婚禮上挠他,老公的妹妹穿的比我還像新娘。我一直安慰自己篡帕,他們只是感情好绩社,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赂苗,像睡著了一般愉耙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拌滋,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天朴沿,我揣著相機與錄音,去河邊找鬼败砂。 笑死赌渣,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的昌犹。 我是一名探鬼主播坚芜,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼斜姥!你這毒婦竟也來了鸿竖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤铸敏,失蹤者是張志新(化名)和其女友劉穎缚忧,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體杈笔,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡闪水,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蒙具。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片球榆。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖禁筏,靈堂內(nèi)的尸體忽然破棺而出持钉,到底是詐尸還是另有隱情,我是刑警寧澤融师,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布右钾,位于F島的核電站蚁吝,受9級特大地震影響旱爆,放射性物質(zhì)發(fā)生泄漏舀射。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一怀伦、第九天 我趴在偏房一處隱蔽的房頂上張望脆烟。 院中可真熱鬧,春花似錦房待、人聲如沸邢羔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拜鹤。三九已至,卻和暖如春流椒,著一層夾襖步出監(jiān)牢的瞬間敏簿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工宣虾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留惯裕,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓绣硝,卻偏偏與公主長得像蜻势,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鹉胖,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

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