ThreadLocal分析及使用注意事項(xiàng)

1.基本介紹

ThreadLocal提供了線程本地變量晌纫,它可以保證訪問到的變量屬于當(dāng)前線程税迷,每個線程都保存有一個變量副本,每個線程的變量都不同锹漱,而同一個線程在任何時候訪問這個本地變量的結(jié)果都是一致的箭养。

2.一個簡單的例子

ThreadLocal通常定義為private static類型。下面是一個簡單的例子了解基本用法哥牍,之后我們研究原理

public class ThreadLocalTest {

    public static class MyRunnable implements Runnable {

        private static ThreadLocal<Integer> threadLocal = new ThreadLocal();

        @Override
        public void run() {
            threadLocal.set((int) (Math.random() * 100D));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
        }
    }

    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
    }
}

輸出(一次可能的輸出)

32
61

3.源碼分析

為了弄清為什么不同線程調(diào)用get方法的時候都是自己的本地變量我們就粗暴的直接看get方法是如何實(shí)現(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();//1.獲取當(dāng)前調(diào)用的線程
        ThreadLocalMap map = getMap(t);//2.獲取ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//3獲取真正的值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

通過源碼我們可以發(fā)現(xiàn)毕泌,1.獲取當(dāng)前調(diào)用的線程2處獲取了一個map,這個具體的實(shí)現(xiàn)如下

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

可以看出這是線程的一個內(nèi)部變量,ThreadLocalMap是ThreadLocal的一個靜態(tài)內(nèi)部類嗅辣。

    //Thread.class

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

下面我們繼續(xù)分析到3處撼泛,這里拿到線程自己的map之后

map.getEntry(this)

也就是theadLocal自身作為key,然后獲取到對應(yīng)的值,看下ThreadLocalMap的部分源碼:

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

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

可以看出存儲的key值本質(zhì)上是threadLocal.threadLocalHashCode.那這個threadLocalHashCode是怎么保證唯一性呢,繼續(xù)看

    private final int threadLocalHashCode = nextHashCode();

    //繼續(xù)跟蹤
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

到這一步澡谭,大體上也許就能明白基本原理愿题,當(dāng)然里面還有許多細(xì)節(jié),希望大家能夠自己看源碼來加深理解蛙奖。

簡單總結(jié):

  • 1.重要的是理清Thread潘酗、ThreadLocal 和ThreadLocalMap三者之間的關(guān)系叔营。
  • 2.ThreadLocalMap解決沖突的方法是線性探測法(不斷加1)熄驼,而不是HashMap的鏈地址法惋增,這一點(diǎn)也能從ThreadLocalMap源碼中Entry的結(jié)構(gòu)上看出來医清。
  • 3.ThreadLocalMap中的Entry的key實(shí)現(xiàn)了弱引用华糖,這樣可以讓無用的key及時的回收作郭,Entry中Value的清理發(fā)生在調(diào)用set()或者remove() 方法是可能會觸發(fā)expungeStaleEntry()方法來清理無用的Entry呢蛤。

4.使用注意點(diǎn)

1.每次使用完ThreadLocal黑毅,都調(diào)用它的remove()方法祭衩,清除數(shù)據(jù)灶体。
2.當(dāng)和線程池使用時尤其要注意,沒有及時清理ThreadLocal掐暮,不僅是內(nèi)存泄漏的問題蝎抽,更嚴(yán)重的是可能導(dǎo)致業(yè)務(wù)邏輯出現(xiàn)問題。
3.線程池和InheritableThreadLocal使用更加需要注意,InheritableThreadLocal并不是線程安全的樟结,怎么說呢养交,這要從InheritableThreadLocal的實(shí)現(xiàn)原理講起。
在新創(chuàng)建線程的時候瓢宦,會調(diào)用new Thead()

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
然后繼續(xù)跟:
private void init(ThreadGroup g, Runnable target, String name,long stackSize) {
    init(g, target, name, stackSize, null);
}
繼續(xù)
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
    ......
    if (parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    ......
}
///////////////////

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

//////////////////
 
/**
 * Construct a new map including all Inheritable ThreadLocals
 * from given parent map. Called only by createInheritedMap.
 *
 * @param parentMap the map associated with parent thread.
 */
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
 
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            ThreadLocal key = e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

最終碎连,會把父線程的ThreadLocalMap 復(fù)制一份到子線程中,劃重點(diǎn)------關(guān)鍵的一句是:

Object value = key.childValue(e.value);

那么 childValue方法是實(shí)現(xiàn)是什么呢驮履?查看源碼發(fā)現(xiàn)InheritableThreadLocal 中的實(shí)現(xiàn)

/**
   * Computes the child's initial value for this inheritable thread-local
   * variable as a function of the parent's value at the time the child
   * thread is created.  This method is called from within the parent
   * thread before the child is started.
   * <p>
   * This method merely returns its input argument, and should be overridden
   * if a different behavior is desired.
   *
   * @param parentValue the parent thread's value
   * @return the child thread's initial value
   */
  protected T childValue(T parentValue) {
      return parentValue;
  }

默認(rèn)直接return 父線程的parentValue鱼辙,所以這樣就導(dǎo)致子線程和父線程指向的是同一個對象,這里并不是值傳遞玫镐,而是引用傳遞倒戏。而且,仔細(xì)看注釋

* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.

注釋說明如果有特定的需求,這個方法應(yīng)該被覆寫!!!
由于返回的是引用恐似,這樣我們線程池中很多的線程都會共用一個ThreadLocal,當(dāng)其中一個更新了ThreadLocal的值后杜跷,就會影響其他的線程,所以就出現(xiàn)了我們最開始提到的場景矫夷,那么如果我非要使用InheritableThreadLocal呢葛闷?方法就是覆寫 childValue方法,保證是值傳遞而不是引用傳遞双藕。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末淑趾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蔓彩,更是在濱河造成了極大的恐慌治笨,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赤嚼,死亡現(xiàn)場離奇詭異旷赖,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)更卒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門等孵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蹂空,你說我怎么就攤上這事俯萌。” “怎么了上枕?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵咐熙,是天一觀的道長。 經(jīng)常有香客問我辨萍,道長棋恼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮爪飘,結(jié)果婚禮上义起,老公的妹妹穿的比我還像新娘。我一直安慰自己师崎,他們只是感情好默终,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著犁罩,像睡著了一般齐蔽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上昼汗,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天肴熏,我揣著相機(jī)與錄音,去河邊找鬼顷窒。 笑死,一個胖子當(dāng)著我的面吹牛源哩,可吹牛的內(nèi)容都是我干的鞋吉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼励烦,長吁一口氣:“原來是場噩夢啊……” “哼谓着!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起坛掠,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤赊锚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后屉栓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舷蒲,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年友多,在試婚紗的時候發(fā)現(xiàn)自己被綠了牲平。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡域滥,死狀恐怖纵柿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情启绰,我是刑警寧澤昂儒,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站委可,受9級特大地震影響渊跋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一刹枉、第九天 我趴在偏房一處隱蔽的房頂上張望叽唱。 院中可真熱鬧,春花似錦微宝、人聲如沸棺亭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽镶摘。三九已至,卻和暖如春岳守,著一層夾襖步出監(jiān)牢的瞬間凄敢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工湿痢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留涝缝,地道東北人。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓譬重,卻偏偏與公主長得像拒逮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子臀规,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

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