ThreadLocal源碼深入分析

ThreadLocal源碼深入分析

ThreadLocal :線程本地存儲(chǔ)區(qū)(Thread Local Storage耀怜,簡(jiǎn)稱為T(mén)LS)袜茧,每個(gè)線程都有自己的私有的本地存儲(chǔ)區(qū)域爪瓜,不同線程之間彼此不能訪問(wèn)對(duì)方的TLS區(qū)域。它是一個(gè)線程內(nèi)部的數(shù)據(jù)類梯捕,通過(guò)它可以在指定線程中存儲(chǔ)數(shù)據(jù)厢呵,數(shù)據(jù)存儲(chǔ)以后,只有在指定線程中可以獲取到存儲(chǔ)的數(shù)據(jù)傀顾,對(duì)于其他線程來(lái)說(shuō)則無(wú)法獲取到數(shù)據(jù)襟铭。一般來(lái)說(shuō),當(dāng)某些數(shù)據(jù)是以線程為作用域并且不同的線程具有不同的數(shù)據(jù)副本的時(shí)候,就可以采用ThreadLocal寒砖。比如對(duì)于Handler來(lái)說(shuō)赐劣,他需要獲取當(dāng)前線程的Looper,很顯然Looper的作用域就是線程入撒,并且不同線程具有不同的Looper隆豹,這個(gè)時(shí)候通過(guò)ThreadLocal就可以輕松實(shí)現(xiàn)Looper在線程中的獲取。下面通過(guò)實(shí)際的例子來(lái)演示ThreadLocal的真正含義茅逮。

1. 實(shí)例

public class TestThreadLocal {
    private static final ThreadLocal mThreadLocal = new ThreadLocal();

    public static void main(String[] args) {
        mThreadLocal.set(true);
        System.out.println("ThreadMain mThreadLocal = "+mThreadLocal.get());
        new Thread("Thread1"){
            @Override
            public void run() {
                mThreadLocal.set(false);
                System.out.println("Thread1 mThreadLocal = "+mThreadLocal.get());
            }
        }.start();

        new Thread("Thread2"){
            @Override
            public void run() {
                System.out.println("Thread2 mThreadLocal = "+mThreadLocal.get());
            }
        }.start();


    }
}

在上面的代碼中,在主線程中設(shè)置 mThreadLocal 的值為 true判哥,在子線程1中設(shè)置 mThreadLocal 的值為 false献雅,然后在子線程2中不設(shè)置 mThreadLocal 的值。然后分別在3個(gè)子線程中通過(guò) get 方法獲取 mThreadLocal 的值塌计,根據(jù)前面對(duì) ThreadLocal 的描述挺身,這個(gè)時(shí)候,主線程應(yīng)該是 true锌仅,子線程1中應(yīng)該是 false章钾,而子線程 2 中由于沒(méi)有設(shè)置值,所以應(yīng)該是 null热芹。

運(yùn)行結(jié)果為:

ThreadMain mThreadLocal = true
Thread1 mThreadLocal = false
Thread2 mThreadLocal = null

從結(jié)果可以看出贱傀, 雖然在不同線程中訪問(wèn)的是同一個(gè) ThreadLocal 對(duì)象,但是它們通過(guò) ThreadLocal 獲取到的值卻不同伊脓。下面從源碼的角度來(lái)分析它的工作原理府寒。

2. 類圖

假如讓我們來(lái)實(shí)現(xiàn)一個(gè)變量與線程相綁定的功能,我們可以很容易地想到用HashMap來(lái)實(shí)現(xiàn)报腔,Thread作為key株搔,變量作為value。事實(shí)上纯蛾,JDK中確實(shí)使用了類似Map的結(jié)構(gòu)存儲(chǔ)變量纤房,但不是像我們想的那樣。下面我們來(lái)探究OpenJDK 1.8中ThreadLocal的實(shí)現(xiàn)翻诉。

diagram.png

3. ThreadLocal的成員變量

我們從 ThreadLocal 的幾個(gè)成員變量入手:

/**
 * ThreadLocals rely on per-thread linear-probe hash maps attached
 * to each thread (Thread.threadLocals and
 * inheritableThreadLocals).  The ThreadLocal objects act as keys,
 * searched via threadLocalHashCode.  This is a custom hash code
 * (useful only within ThreadLocalMaps) that eliminates collisions
 * in the common case where consecutively constructed ThreadLocals
 * are used by the same threads, while remaining well-behaved in
 * less common cases.
 */
private final int threadLocalHashCode = nextHashCode();

/**
 * The next hash code to be given out. Updated atomically. Starts at
 * zero.
 */
private static AtomicInteger nextHashCode =
    new AtomicInteger();

/**
 * The difference between successively generated hash codes - turns
 * implicit sequential thread-local IDs into near-optimally spread
 * multiplicative hash values for power-of-two-sized tables.
 */
private static final int HASH_INCREMENT = 0x61c88647;

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

ThreadLocal通過(guò)threadLocalHashCode 來(lái)標(biāo)識(shí)每一個(gè)ThreadLocal 的唯一性炮姨。threadLocalHashCode 通過(guò) CAS操作 進(jìn)行更新,每次 hash 操作的增量為 0x61c88647 米丘。

4. ThreadLocal的重要方法

4.1 ThreadLocal#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);
}

可以看到通過(guò)Thread.currentThread()方法獲取了當(dāng)前的線程引用剑令,并傳給了getMap(Thread)方法獲取一個(gè)ThreadLocalMap的實(shí)例。我們繼續(xù)跟進(jìn)getMap(Thread)方法:

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

可以看到getMap(Thread)方法直接返回Thread實(shí)例的成員變量threadLocals拄查。它的定義在Thread內(nèi)部吁津,訪問(wèn)級(jí)別為package級(jí)別:

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

到了這里,我們可以看出,每個(gè)Thread里面都有一個(gè)ThreadLocal.ThreadLocalMap成員變量碍脏,也就是說(shuō)每個(gè)線程通過(guò)ThreadLocal.ThreadLocalMap與ThreadLocal相綁定梭依,這樣可以確保每個(gè)線程訪問(wèn)到的thread-local variable都是本線程的。

我們往下繼續(xù)分析典尾。獲取了ThreadLocalMap實(shí)例以后役拴,如果它不為空則調(diào)用ThreadLocalMap.ThreadLocalMap#set方法設(shè)值;若為空則調(diào)用ThreadLocal#createMap方法new一個(gè)ThreadLocalMap實(shí)例并賦給Thread.threadLocals钾埂。

ThreadLocal#createMap方法的源碼如下:

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

4.2 ThreadLocal#get方法

ThreadLocal的get方法就是調(diào)用了ThreadLocalMap的getEntry方法:

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

可以看到 ThreadLocal 的 getset 方法調(diào)用了 ThreadLocalMap 中的 setgetEntry 方法河闰。下面我們探究一下ThreadLocalMap的實(shí)現(xiàn)

5. ThreadLocalMap

5.1 ThreadLocal的成員變量

在上面的類圖中,我們可以看到 ThreadLocalMap 有一個(gè)常量和三個(gè)成員變量:

/**
 * The initial capacity -- MUST be a power of two.
 */
private static final int INITIAL_CAPACITY = 16;

/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
 */
private Entry[] table;

/**
 * The number of entries in the table.
 */
private int size = 0;

/**
 * The next size value at which to resize.
 */
private int threshold; // Default to 0

其中INITIAL_CAPACITY代表這個(gè)Map的初始容量褥紫;1是一個(gè)Entry類型的數(shù)組姜性,用于存儲(chǔ)數(shù)據(jù);size代表表中的存儲(chǔ)數(shù)目髓考;threshold代表需要擴(kuò)容時(shí)對(duì)應(yīng)size的閾值部念。

Entry類是ThreadLocalMap的靜態(tài)內(nèi)部類,用于存儲(chǔ)數(shù)據(jù)氨菇。它的源碼如下:

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

Entry類繼承了WeakReference<ThreadLocal<?>>儡炼,即每個(gè)Entry對(duì)象都有一個(gè)ThreadLocal的弱引用(作為key)乌询,這是為了防止內(nèi)存泄露。一旦線程結(jié)束楣责,key變?yōu)橐粋€(gè)不可達(dá)的對(duì)象,這個(gè)Entry就可以被GC了秆麸。

5.2 ThreadLocalMap的構(gòu)造函數(shù)

ThreadLocalMap類有兩個(gè)構(gòu)造函數(shù)及汉,其中常用的是ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)

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

構(gòu)造函數(shù)的第一個(gè)參數(shù)就是本ThreadLocal實(shí)例(this),第二個(gè)參數(shù)就是要保存的線程本地變量房铭。構(gòu)造函數(shù)首先創(chuàng)建一個(gè)長(zhǎng)度為16的Entry數(shù)組,然后計(jì)算出firstKey對(duì)應(yīng)的哈希值缸匪,然后存儲(chǔ)到table中类溢,并設(shè)置size和threshold露懒。

注意一個(gè)細(xì)節(jié),計(jì)算hash的時(shí)候里面采用了hashCode & (size - 1)的算法砂心,這相當(dāng)于取模運(yùn)算hashCode % size的一個(gè)更高效的實(shí)現(xiàn)(和HashMap中的思路相同)懈词。正是因?yàn)檫@種算法,我們要求size必須是2的指數(shù)辩诞,因?yàn)檫@可以使得hash發(fā)生沖突的次數(shù)減小坎弯。

5.3 ThreadLocalMap#set方法

接下來(lái)我們來(lái)看ThreadLocalMap#set方法的實(shí)現(xiàn):

/**
 * 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);
    //線性探測(cè)
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        //找到對(duì)應(yīng)的key
        if (k == key) {
            e.value = value;
            return;
        }
        // 替換失效的key
        if (k == null) {
            //如果entry里對(duì)應(yīng)的key為null的話,表明此entry為staled entry (staled 舊的)译暂,就將其替換為             //當(dāng)前的key和value抠忘。詳情可見(jiàn)replaceStaleEntry 方法。
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //若是經(jīng)歷了上面步驟沒(méi)有命中hash秧秉,也沒(méi)有發(fā)現(xiàn)無(wú)用的Entry褐桌,set方法就會(huì)創(chuàng)建一個(gè)新的Entry,并會(huì)進(jìn)行啟發(fā)     //式的垃圾清理象迎,用于清理無(wú)用的Entry。如果size大于閾值呛踊,還會(huì)進(jìn)行rehash()算法砾淌。
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

如果沖突了,就會(huì)通過(guò)nextIndex方法再次計(jì)算哈希值:

/**
 * Increment i modulo len.
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

到這里谭网,我們看到ThreadLocalMap解決沖突的方法是線性探測(cè)法(不斷加1)汪厨,而不是HashMap的鏈地址法,這一點(diǎn)也能從Entry的結(jié)構(gòu)上推斷出來(lái)愉择。

5.4 ThreadLocalMap#replaceStaleEntry方法

/**
 * Replace a stale entry encountered during a set operation
 * with an entry for the specified key.  The value passed in
 * the value parameter is stored in the entry, whether or not
 * an entry already exists for the specified key.
 *
 * As a side effect, this method expunges all stale entries in the
 * "run" containing the stale entry.  (A run is a sequence of entries
 * between two null slots.)
 *
 * @param  key the key
 * @param  value the value to be associated with key
 * @param  staleSlot index of the first stale entry encountered while
 *         searching for key.
 */
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    //向前掃描劫乱,查找最前的一個(gè)無(wú)效slot
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    //向后遍歷table
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        //// 找到了key,將其與無(wú)效的slot交換
        if (k == key) {
            e.value = value;

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

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        //// 如果當(dāng)前的slot已經(jīng)無(wú)效锥涕,并且向前掃描過(guò)程中沒(méi)有無(wú)效slot衷戈,則更新slotToExpunge為當(dāng)前位置
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    // 在探測(cè)過(guò)程中如果發(fā)現(xiàn)任何無(wú)效slot,則做一次清理(連續(xù)段清理+啟發(fā)式清理)
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

替換過(guò)程里面也進(jìn)行了不少的垃圾清理動(dòng)作以防止引用關(guān)系存在而導(dǎo)致的內(nèi)存泄露层坠。主要是cleanSomeSlots

5.5 ThreadLocalMap的清理算法#cleanSomeSlots和#expungeStaleEntry算法

/**
 * 啟發(fā)式地清理slot,
 * i對(duì)應(yīng)entry是非無(wú)效(指向的ThreadLocal沒(méi)被回收谦趣,或者entry本身為空)
 * n是用于控制控制掃描次數(shù)的
 * 正常情況下如果log n次掃描沒(méi)有發(fā)現(xiàn)無(wú)效slot前鹅,函數(shù)就結(jié)束了
 * 但是如果發(fā)現(xiàn)了無(wú)效的slot峭梳,將n置為table的長(zhǎng)度len,做一次連續(xù)段的清理
 * 再?gòu)南乱粋€(gè)空的slot開(kāi)始繼續(xù)掃描
 * 
 * 這個(gè)函數(shù)有兩處地方會(huì)被調(diào)用叉橱,一處是插入的時(shí)候可能會(huì)被調(diào)用窃祝,另外個(gè)是在替換無(wú)效slot的時(shí)候可能會(huì)被調(diào)用粪小,
 * 區(qū)別是前者傳入的n為元素個(gè)數(shù)抡句,后者為table的容量
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        // i在任何情況下自己都不會(huì)是一個(gè)無(wú)效slot待榔,所以從下一個(gè)開(kāi)始判斷
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            // 擴(kuò)大掃描控制因子
            n = len;
            removed = true;
            //  清理一個(gè)連續(xù)段
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

/**
 * 這個(gè)函數(shù)是ThreadLocal中核心清理函數(shù)腌闯,它做的事情很簡(jiǎn)單:
 * 就是從staleSlot開(kāi)始遍歷雕憔,將無(wú)效(弱引用指向?qū)ο蟊换厥眨┣謇斫锉耍磳?duì)應(yīng)entry中的value置為null琉苇,將指向這   entry的table[i]置為null,直到掃到空entry趁冈。
 * 另外渗勘,在過(guò)程中還會(huì)對(duì)非空的entry作rehash旺坠。
 * 可以說(shuō)這個(gè)函數(shù)的作用就是從staleSlot開(kāi)始清理連續(xù)段中的slot(斷開(kāi)強(qiáng)引用取刃,rehash slot等)
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 因?yàn)閑ntry對(duì)應(yīng)的ThreadLocal已經(jīng)被回收,value設(shè)為null坯辩,顯式斷開(kāi)強(qiáng)引用
    tab[staleSlot].value = null;
    // 顯式設(shè)置該entry為null漆魔,以便垃圾回收
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 清理對(duì)應(yīng)ThreadLocal已經(jīng)被回收的entry
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            /*
             * 對(duì)于還沒(méi)有被回收的情況改抡,需要做一次rehash阿纤。
             * 
             * 如果對(duì)應(yīng)的ThreadLocal的ID對(duì)len取模出來(lái)的索引h不為當(dāng)前位置i欠拾,
             * 則從h向后線性探測(cè)到第一個(gè)空的slot清蚀,把當(dāng)前的entry給挪過(guò)去。
             */
            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.
                 *
                 * 這段話提及了Knuth高德納的著作TAOCP(《計(jì)算機(jī)程序設(shè)計(jì)藝術(shù)》)的6.4章節(jié)(散列)
                 * 中的R算法腹泌。R算法描述了如何從使用線性探測(cè)的散列表中刪除一個(gè)元素尔觉。
                 * R算法維護(hù)了一個(gè)上次刪除元素的index侦铜,當(dāng)在非空連續(xù)段中掃到某個(gè)entry的哈希值取模后的索                  引
                 * 還沒(méi)有遍歷到時(shí)钉稍,會(huì)將該entry挪到index那個(gè)位置贡未,并更新當(dāng)前位置為新的index,
                 * 繼續(xù)向后掃描直到遇到空的entry嫩挤。
                 *
                 * ThreadLocalMap因?yàn)槭褂昧巳跻闷裾眩云鋵?shí)每個(gè)slot的狀態(tài)有三種也即
                 * 有效(value未回收),無(wú)效(value已回收)叼风,空(entry==null)无宿。
                 * 正是因?yàn)門(mén)hreadLocalMap的entry有三種狀態(tài)孽鸡,所以不能完全套高德納原書(shū)的R算法彬碱。
                 *
                 * 因?yàn)閑xpungeStaleEntry函數(shù)在掃描過(guò)程中還會(huì)對(duì)無(wú)效slot清理將之轉(zhuǎn)為空slot奥洼,
                 * 如果直接套用R算法灵奖,可能會(huì)出現(xiàn)具有相同哈希值的entry之間斷開(kāi)(中間有空entry)瓷患。
                 */
                while (tab[h] != null) {
                    h = nextIndex(h, len);
                }
                tab[h] = e;
            }
        }
    }
    // 返回staleSlot之后第一個(gè)空的slot索引
    return i;
}

5.6 ThreadLocalMap#rehash()方法

/**
 * Re-pack and/or re-size the table. First scan the entire
 * table removing stale entries. If this doesn't sufficiently
 * shrink the size of the table, double the table size.
 */
private void rehash() {
    // 做一次全量清理
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    /*
     * 因?yàn)樽隽艘淮吻謇砩帽啵詓ize很可能會(huì)變小爱态。
     * ThreadLocalMap這里的實(shí)現(xiàn)是調(diào)低閾值來(lái)判斷是否需要擴(kuò)容肢藐,
     * threshold默認(rèn)為len*2/3吆豹,所以這里的threshold - threshold / 4相當(dāng)于len/2
     */
    if (size >= threshold - threshold / 4)
        resize();
}

/**
 * Double the capacity of the table.
 */
/**
 * 擴(kuò)容理盆,因?yàn)樾枰WCtable的容量len為2的冪猿规,所以擴(kuò)容即擴(kuò)大2倍
 */
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

/**
 * Expunge all stale entries in the table.
 */
 /*
 * 做一次全量清理
 */
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

我們來(lái)回顧一下 ThreadLocalset 方法可能會(huì)有的情況

  • 探測(cè)過(guò)程中slot都不無(wú)效,并且順利找到key所在的slot环葵,直接替換即可
  • 探測(cè)過(guò)程中發(fā)現(xiàn)有無(wú)效slot张遭,調(diào)用replaceStaleEntry地梨,效果是最終一定會(huì)把key和value放在這個(gè)slot宝剖,并且會(huì)盡可能清理無(wú)效slot
    • 在replaceStaleEntry過(guò)程中万细,如果找到了key,則做一個(gè)swap把它放到那個(gè)無(wú)效slot中襟雷,value置為新值
    • 在replaceStaleEntry過(guò)程中,沒(méi)有找到key卓缰,直接在無(wú)效slot原地放entry
  • 探測(cè)沒(méi)有發(fā)現(xiàn)key征唬,則在連續(xù)段末尾的后一個(gè)空位置放上entry总寒,這也是線性探測(cè)法的一部分理肺。放完后,做一次啟發(fā)式清理炫欺,如果沒(méi)清理出去key熏兄,并且當(dāng)前table大小已經(jīng)超過(guò)閾值了摩桶,則做一次rehash硝清,rehash函數(shù)會(huì)調(diào)用一次全量清理slot方法也即expungeStaleEntries耍缴,如果完了之后table大小超過(guò)了threshold - threshold / 4袋马,則進(jìn)行擴(kuò)容2倍

5.7 ThreadLocalMap#getEntry方法

/**
 * Get the entry associated with key.  This method
 * itself handles only the fast path: a direct hit of existing
 * key. It otherwise relays to getEntryAfterMiss.  This is
 * designed to maximize performance for direct hits, in part
 * by making this method readily inlinable.
 *
 * @param  key the thread local object
 * @return the entry associated with key, or null if no such
 */
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

/**
 * Version of getEntry method for use when key is not found in
 * its direct hash slot.
 *
 * @param  key the thread local object
 * @param  i the table index for key's hash code
 * @param  e the entry at table[i]
 * @return the entry associated with key, or null if no such
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

從ThreadLocal讀一個(gè)值可能遇到的情況:
根據(jù)入?yún)hreadLocal的threadLocalHashCode對(duì)表容量取模得到index

  • 如果index對(duì)應(yīng)的slot就是要讀的threadLocal,則直接返回結(jié)果
  • 調(diào)用getEntryAfterMiss線性探測(cè)泄鹏,過(guò)程中每碰到無(wú)效slot番官,調(diào)用expungeStaleEntry進(jìn)行段清理徘熔;如果找到了key酷师,則返回結(jié)果entry
  • 沒(méi)有找到key山孔,返回null荷憋。

5.8 ThreadLocalMap#remove方法

/**
 * 從map中刪除ThreadLocal
 */
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) {
            // 顯式斷開(kāi)弱引用
            e.clear();
            // 進(jìn)行段清理
            expungeStaleEntry(i);
            return;
        }
    }
}

remove方法相對(duì)于getEntry和set方法比較簡(jiǎn)單串前,直接在table中找key,如果找到了减宣,把弱引用斷了做一次段清理玩荠。

6. ThreadLocal與內(nèi)存泄漏

關(guān)于ThreadLocal是否會(huì)引起內(nèi)存泄漏也是一個(gè)比較有爭(zhēng)議性的問(wèn)題阶冈,其實(shí)就是要看對(duì)內(nèi)存泄漏的準(zhǔn)確定義是什么女坑。
認(rèn)為T(mén)hreadLocal會(huì)引起內(nèi)存泄漏的說(shuō)法是因?yàn)槿绻粋€(gè)ThreadLocal對(duì)象被回收了匆骗,我們往里面放的value對(duì)于【當(dāng)前線程->當(dāng)前線程的threadLocals(ThreadLocal.ThreadLocalMap對(duì)象)->Entry數(shù)組->某個(gè)entry.value】這樣一條強(qiáng)引用鏈?zhǔn)强蛇_(dá)的碉就,因此value不會(huì)被回收。
認(rèn)為T(mén)hreadLocal不會(huì)引起內(nèi)存泄漏的說(shuō)法是因?yàn)門(mén)hreadLocal.ThreadLocalMap源碼實(shí)現(xiàn)中自帶一套自我清理的機(jī)制筋量。

之所以有關(guān)于內(nèi)存泄露的討論是因?yàn)樵谟芯€程復(fù)用如線程池的場(chǎng)景中桨武,一個(gè)線程的壽命很長(zhǎng)呀酸,大對(duì)象長(zhǎng)期不被回收影響系統(tǒng)運(yùn)行效率與安全。如果線程不會(huì)復(fù)用七咧,用完即銷(xiāo)毀了也不會(huì)有ThreadLocal引發(fā)內(nèi)存泄露的問(wèn)題”妫《Effective Java》一書(shū)中的第6條對(duì)這種內(nèi)存泄露稱為unintentional object retention(無(wú)意識(shí)的對(duì)象保留)先较。

當(dāng)我們仔細(xì)讀過(guò)ThreadLocalMap的源碼悼粮,我們可以推斷扣猫,如果在使用的ThreadLocal的過(guò)程中申尤,顯式地進(jìn)行remove是個(gè)很好的編碼習(xí)慣昧穿,這樣是不會(huì)引起內(nèi)存泄漏时鸵。
那么如果沒(méi)有顯式地進(jìn)行remove呢厅瞎?只能說(shuō)如果對(duì)應(yīng)線程之后調(diào)用ThreadLocal的get和set方法都有很高的概率會(huì)順便清理掉無(wú)效對(duì)象和簸,斷開(kāi)value強(qiáng)引用比搭,從而大對(duì)象被收集器回收。

但無(wú)論如何蜜托,我們應(yīng)該考慮到何時(shí)調(diào)用ThreadLocal的remove方法橄务。一個(gè)比較熟悉的場(chǎng)景就是對(duì)于一個(gè)請(qǐng)求一個(gè)線程的server如tomcat蜂挪,在代碼中對(duì)web api作一個(gè)切面嗓化,存放一些如用戶名等用戶信息刺覆,在連接點(diǎn)方法結(jié)束后,再顯式調(diào)用remove篇梭。

7. 總結(jié)

每個(gè)Thread里都含有一個(gè)ThreadLocalMap的成員變量酝枢,這種機(jī)制將ThreadLocal和線程巧妙地綁定在了一起帘睦,即可以保證無(wú)用的ThreadLocal被及時(shí)回收官脓,不會(huì)造成內(nèi)存泄露卑笨,又可以提升性能。假如我們把ThreadLocalMap做成一個(gè)Map<t extends Thread, ?>類型的Map妖滔,那么它存儲(chǔ)的東西將會(huì)非常多(相當(dāng)于一張全局線程本地變量表)座舍,這樣的情況下用線性探測(cè)法解決哈希沖突的問(wèn)題效率會(huì)非常差曲秉。而JDK里的這種利用ThreadLocal作為key承二,再將ThreadLocalMap與線程相綁定的實(shí)現(xiàn)纲爸,完美地解決了這個(gè)問(wèn)題。

本文參考了以下兩篇文章负蚊。特別是文章1家妆,里面對(duì)ThreadLocal中的清理算法做了詳細(xì)的注釋伤极。感謝兩篇文章作者的分享。

[1]http://www.cnblogs.com/micrari/p/6790229.html

[2]http://www.sczyh30.com/posts/Java/java-concurrent-threadlocal/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市齿税,隨后出現(xiàn)的幾起案子凌箕,更是在濱河造成了極大的恐慌牵舱,老刑警劉巖芜壁,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慧妄,死亡現(xiàn)場(chǎng)離奇詭異塞淹,居然都是意外死亡饱普,警方通過(guò)查閱死者的電腦和手機(jī)套耕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)箍铲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)颠猴,“玉大人翘瓮,你說(shuō)我怎么就攤上這事资盅。” “怎么了每庆?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵缤灵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我芝薇,道長(zhǎng),這世上最難降的妖魔是什么馋劈? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任侣滩,我火速辦了婚禮君珠,結(jié)果婚禮上娇斑,老公的妹妹穿的比我還像新娘唯竹。我一直安慰自己浸颓,他們只是感情好产上,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著沉桌,像睡著了一般算吩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上佃扼,一...
    開(kāi)封第一講書(shū)人閱讀 51,365評(píng)論 1 302
  • 那天偎巢,我揣著相機(jī)與錄音,去河邊找鬼兼耀。 笑死艘狭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的翠订。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼遵倦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼尽超!你這毒婦竟也來(lái)了掠哥?” 一聲冷哼從身側(cè)響起塞琼,我...
    開(kāi)封第一講書(shū)人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤派近,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡错沃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年醒叁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吁伺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡夸赫,死狀恐怖胁附,靈堂內(nèi)的尸體忽然破棺而出揭绑,到底是詐尸還是另有隱情菇存,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宿崭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一吆鹤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸姑尺。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春扮匠,著一層夾襖步出監(jiān)牢的瞬間力麸,已是汗流浹背埃叭。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人恼琼。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像已日,于是被迫代替她去往敵國(guó)和親缔莲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子松靡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

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