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

    }

輸出結(jié)果:

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

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

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

可以看到返回了當前線程的threadLocals屬性,當該屬性不為空時調(diào)用其對應(yīng)的set方法,否則調(diào)用createMap方法進行初始化,首先來看一下createMap方法

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

這里主要做的事情是初始化當前線程的threadLocals,來看一下構(gòu)造方法

        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因為弱引用的關(guān)系回收了,但該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實質(zhì)是一個以指定步長進行累加的累加器,該步長能較好的將連續(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為臟對象時,會調(diào)用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,則會調(diào)用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位置設(shè)為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)對應(yīng)的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)結(jié)束仍未找到對應(yīng)的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取出對應(yīng)的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設(shè)置了null值,通過當前線程獲取threadLocals,若map存在則調(diào)用set方法,否則調(diào)用createMap方法創(chuàng)建threadLocals。

總結(jié)和思考

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

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

對于問題1,由于ThreadLocal的生命周期普遍長于Thread,因此當Thread生命周期結(jié)束以后,即使ThreadLocal仍存在,但由于弱引用的關(guān)系,ThreadLocalMap就可以被釋放了。

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容