一文了解ThreadLocal

1. ThreadLocal的簡介

在多線程編程中通常解決線程安全的問題我們會利用synchronzed或者lock控制線程對臨界區資源的同步順序從而解決線程安全的問題,但是這種加鎖的方式會讓未獲取到鎖的線程進行阻塞等待,很顯然這種方式的時間效率並不是很好。線程安全問題的核心在於多個線程會對同一個臨界區共享資源進行操作,那麼,如果每個線程都使用自己的“共享資源”,各自使用各自的,又互相不影響到彼此即讓多個線程間達到隔離的狀態,這樣就不會出現線程安全的問題。事實上,這就是一種“空間換時間”的方案,每個線程都會都擁有自己的“共享資源”無疑內存會大很多,但是由於不需要同步也就減少了線程可能存在的阻塞等待的情況從而提高了時間效率

雖然ThreadLocal並不在java.util.concurrent包中而在java.lang包中,但我更傾向於把它當作是一種併發容器(雖然真正存放數據的是ThreadLoclMap)進行歸類。從ThreadLocal這個類名可以顧名思義的進行理解,表示線程的“本地變量”,即每個線程都擁有該變量副本,達到人手一份的效果,各用各的這樣就可以避免共享資源的競爭。

2. ThreadLocal的實現原理

要想學習到ThreadLocal的實現原理,就必須瞭解它的幾個核心方法,包括怎樣存怎樣取等等,下面我們一個個來看。

void set(T value)

set方法設置在當前線程中threadLocal變量的值,該方法的源碼爲:

public void set(T value) {
	//1. 獲取當前線程實例對象
    Thread t = Thread.currentThread();
	//2. 通過當前線程實例獲取到ThreadLocalMap對象
    ThreadLocalMap map = getMap(t);
    if (map != null)
		//3. 如果Map不爲null,則以當前threadLocl實例爲key,值爲value進行存入
        map.set(this, value);
    else
		//4.map爲null,則新建ThreadLocalMap並存入value
        createMap(t, value);
}

方法的邏輯很清晰,具體請看上面的註釋。通過源碼我們知道value是存放在了ThreadLocalMap裏了,當前先把它理解爲一個普普通通的map即可,也就是說,數據value是真正的存放在了ThreadLocalMap這個容器中了,並且是以當前threadLocal實例爲key。先簡單的看下ThreadLocalMap是什麼,有個簡單的認識就好,下面會具體說的。

首先ThreadLocalMap是怎樣來的?源碼很清楚,是通過getMap(t)進行獲取:

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

該方法直接返回的就是當前線程對象t的一個成員變量threadLocals:

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

也就是說ThreadLocalMap的引用是作爲Thread的一個成員變量,被Thread進行維護的。回過頭再來看看set方法,當map爲Null的時候會通過createMap(t,value)方法:

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

該方法就是new一個ThreadLocalMap實例對象,然後同樣以當前threadLocal實例作爲key,值爲value存放到threadLocalMap中,然後將當前線程對象的threadLocals賦值爲threadLocalMap。

現在來對set方法進行總結一下: 通過當前線程對象thread獲取該thread所維護的threadLocalMap,若threadLocalMap不爲null,則以threadLocal實例爲key,值爲value的鍵值對存入threadLocalMap,若threadLocalMap爲null的話,就新建threadLocalMap然後在以threadLocal爲鍵,值爲value的鍵值對存入即可。

T get()

get方法是獲取當前線程中threadLocal變量的值,同樣的還是來看看源碼:

public T get() {
	//1. 獲取當前線程的實例對象
    Thread t = Thread.currentThread();
	//2. 獲取當前線程的threadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
		//3. 獲取map中當前threadLocal實例爲key的值的entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
			//4. 當前entitiy不爲null的話,就返回相應的值value
            T result = (T)e.value;
            return result;
        }
    }
	//5. 若map爲null或者entry爲null的話通過該方法初始化,並返回該方法返回的value
    return setInitialValue();
}

弄懂了set方法的邏輯,看get方法只需要帶着逆向思維去看就好,如果是那樣存的,反過來去拿就好。代碼邏輯請看註釋,另外,看下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;
}

這段方法的邏輯和set方法幾乎一致,另外值得關注的是initialValue方法:

protected T initialValue() {
    return null;
}

這個方法是protected修飾的也就是說繼承ThreadLocal的子類可重寫該方法,實現賦值爲其他的初始值。關於get方法來總結一下:

通過當前線程thread實例獲取到它所維護的threadLocalMap,然後以當前threadLocal實例爲key獲取該map中的鍵值對(Entry),若Entry不爲null則返回Entry的value。如果獲取threadLocalMap爲null或者Entry爲null的話,就以當前threadLocal爲Key,value爲null存入map後,並返回null。

void remove()

public void remove() {
	//1. 獲取當前線程的threadLocalMap
	ThreadLocalMap m = getMap(Thread.currentThread());
 	if (m != null)
		//2. 從map中刪除以當前threadLocal實例爲key的鍵值對
		m.remove(this);
}

get,set方法實現了存數據和讀數據,我們當然還得學會如何刪數據。刪除數據當然是從map中刪除數據,先獲取與當前線程相關聯的threadLocalMap然後從map中刪除該threadLocal實例爲key的鍵值對即可。

3. ThreadLocalMap詳解

從上面的分析我們已經知道,數據其實都放在了threadLocalMap中,threadLocal的get,set和remove方法實際上具體是通過threadLocalMap的getEntry,set和remove方法實現的。如果想真正全方位的弄懂threadLocal,勢必得在對threadLocalMap做一番理解。

3.1 Entry數據結構

ThreadLocalMap是threadLocal一個靜態內部類,和大多數容器一樣內部維護了一個數組,同樣的threadLocalMap內部維護了一個Entry類型的table數組

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

通過註釋可以看出,table數組的長度爲2的冪次方。接下來看下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是一個以ThreadLocal爲key,Object爲value的鍵值對,另外需要注意的是這裏的threadLocal是弱引用,因爲Entry繼承了WeakReference,在Entry的構造方法中,調用了super(k)方法就會將threadLocal實例包裝成一個WeakReferenece。到這裏我們可以用一個圖(下圖來自http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/)來理解下thread,threadLocal,threadLocalMap,Entry之間的關係:
在這裏插入圖片描述
注意上圖中的實線表示強引用,虛線表示弱引用。如圖所示,每個線程實例中可以通過threadLocals獲取到threadLocalMap,而threadLocalMap實際上就是一個以threadLocal實例爲key,任意對象爲value的Entry數組。當我們爲threadLocal變量賦值,實際上就是以當前threadLocal實例爲key,值爲value的Entry往這個threadLocalMap中存放。需要注意的是Entry中的key是弱引用,當threadLocal外部強引用被置爲null(threadLocalInstance=null),那麼系統 GC 的時候,根據可達性分析,這個threadLocal實例就沒有任何一條鏈路能夠引用到它,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key爲null的Entry,就沒有辦法訪問這些key爲null的Entry的value,如果當前線程再遲遲不結束的話,這些key爲null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收,造成內存泄漏。當然,如果當前thread運行結束,threadLocal,threadLocalMap,Entry沒有引用鏈可達,在垃圾回收的時候都會被系統進行回收。在實際開發中,會使用線程池去維護線程的創建和複用,比如固定大小的線程池,線程爲了複用是不會主動結束的,所以,threadLocal的內存泄漏問題,是應該值得我們思考和注意的問題。

3.2 針對內存泄漏的優化

實際上,爲了解決threadLocal潛在的內存泄漏的問題,Josh Bloch and Doug Lea大師已經做了一些改進。在threadLocal的set和get方法中都有相應的處理。下文爲了敘述,針對key爲null的entry,源碼註釋爲stale entry,直譯爲不新鮮的entry,這裏我就稱之爲“髒entry”。比如在ThreadLocalMap的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();
}

在該方法中針對髒entry做了這樣的處理:

如果當前table[i]!=null的話說明hash衝突就需要向後環形查找,若在查找過程中遇到髒entry就通過replaceStaleEntry進行處理;
如果當前table[i]==null的話說明新的entry可以直接插入,但是插入後會調用cleanSomeSlots方法檢測並清除髒entry。

1. cleanSomeSlots
該方法的源碼爲:

/* @param i a position known NOT to hold a stale entry. The
 * scan starts at the element after i.
 *
 * @param n scan control: {@code log2(n)} cells are scanned,
 * unless a stale entry is found, in which case
 * {@code log2(table.length)-1} additional cells are scanned.
 * When called from insertions, this parameter is the number
 * of elements, but when from replaceStaleEntry, it is the
 * table length. (Note: all this could be changed to be either
 * more or less aggressive by weighting n instead of just
 * using straight log n. But this version is simple, fast, and
 * seems to work well.)
 *
 * @return true if any stale entries have been removed.
 */
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);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

分析:
入參:
i表示
:插入entry的位置i,很顯然在上述情況2(table[i]==null)中,entry剛插入後該位置i很顯然不是髒entry;
參數n
n的用途
主要用於掃描控制(scan control),從while中是通過n來進行條件判斷的說明n就是用來控制掃描趟數(循環次數)的。在掃描過程中,如果沒有遇到髒entry就整個掃描過程持續log2(n)次,log2(n)的得來是因爲n >>>= 1,每次n右移一位相當於n除以2。如果在掃描過程中遇到髒entry的話就會令n爲當前hash表的長度(n=len),再掃描log2(n)趟,注意此時n增加無非就是多增加了循環次數從而通過nextIndex往後搜索的範圍擴大,示意圖如下
在這裏插入圖片描述
按照n的初始值,搜索範圍爲黑線,當遇到了髒entry,此時n變成了哈希數組的長度(n取值增大),搜索範圍log2(n)增大,紅線表示。如果在整個搜索過程沒遇到髒entry的話,搜索結束,採用這種方式的主要是用於時間效率上的平衡。

n的取值
如果是在set方法插入新的entry後調用(上述情況2),n位當前已經插入的entry個數size;如果是在replaceSateleEntry方法中調用n爲哈希表的長度len。

2. expungeStaleEntry
如果對輸入參數能夠理解的話,那麼cleanSomeSlots方法搜索基本上清除了,但是全部搞定還需要掌握expungeStaleEntry方法,當在搜索過程中遇到了髒entry的話就會調用該方法去清理掉髒entry。源碼爲:

/**
 * Expunge a stale entry by rehashing any possibly colliding entries
 * lying between staleSlot and the next null slot.  This also expunges
 * any other stale entries encountered before the trailing null.  See
 * Knuth, Section 6.4
 *
 * @param staleSlot index of slot known to have null key
 * @return the index of the next null slot after staleSlot
 * (all between staleSlot and this slot will have been checked
 * for expunging).
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

	//清除當前髒entry
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
	//2.往後環形繼續查找,直到遇到table[i]==null時結束
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
		//3. 如果在向後搜索過程中再次遇到髒entry,同樣將其清理掉
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
			//處理rehash的情況
            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;
}

該方法邏輯請看註釋(第1,2,3步),主要做了這麼幾件事情:

  1. 清理當前髒entry,即將其value引用置爲null,並且將table[staleSlot]也置爲null。value置爲null後該value域變爲不可達,在下一次gc的時候就會被回收掉,同時table[staleSlot]爲null後以便於存放新的entry;
  2. 從當前staleSlot位置向後環形(nextIndex)繼續搜索,直到遇到哈希桶(tab[i])爲null的時候退出;
  3. 若在搜索過程再次遇到髒entry,繼續將其清除。

也就是說該方法,清理掉當前髒entry後,並沒有閒下來繼續向後搜索,若再次遇到髒entry繼續將其清理,直到哈希桶(table[i])爲null時退出。因此方法執行完的結果爲 從當前髒entry(staleSlot)位到返回的i位,這中間所有的entry不是髒entry。爲什麼是遇到null退出呢?原因是存在髒entry的前提條件是 當前哈希桶(table[i])不爲null,只是該entry的key域爲null。如果遇到哈希桶爲null,很顯然它連成爲髒entry的前提條件都不具備。

現在對cleanSomeSlot方法做一下總結,其方法執行示意圖如下:
在這裏插入圖片描述
如圖所示,cleanSomeSlot方法主要有這樣幾點:

  1. 從當前位置i處(位於i處的entry一定不是髒entry)爲起點在初始小範圍(log2(n),n爲哈希表已插入entry的個數size)開始向後搜索髒entry,若在整個搜索過程沒有髒entry,方法結束退出
  2. 如果在搜索過程中遇到髒entryt通過expungeStaleEntry方法清理掉當前髒entry,並且該方法會返回下一個哈希桶(table[i])爲null的索引位置爲i。這時重新令搜索起點爲索引位置i,n爲哈希表的長度len,再次擴大搜索範圍爲log2(n’)繼續搜索。

下面,以一個例子更清晰的來說一下,假設當前table數組的情況如下圖。
在這裏插入圖片描述

  1. 如圖當前n等於hash表的size即n=10,i=1,在第一趟搜索過程中通過nextIndex,i指向了索引爲2的位置,此時table[2]爲null,說明第一趟未發現髒entry,則第一趟結束進行第二趟的搜索。

  2. 第二趟所搜先通過nextIndex方法,索引由2的位置變成了i=3,當前table[3]!=null但是該entry的key爲null,說明找到了一個髒entry,先將n置爲哈希表的長度len,然後繼續調用expungeStaleEntry方法,該方法會將當前索引爲3的髒entry給清除掉(令value爲null,並且table[3]也爲null),但是該方法可不想偷懶,它會繼續往後環形搜索,往後會發現索引爲4,5的位置的entry同樣爲髒entry,索引爲6的位置的entry不是髒entry保持不變,直至i=7的時候此處table[7]位null,該方法就以i=7返回。至此,第二趟搜索結束;

  3. 由於在第二趟搜索中發現髒entry,n增大爲數組的長度len,因此擴大搜索範圍(增大循環次數)繼續向後環形搜索;

  4. 直到在整個搜索範圍裏都未發現髒entry,cleanSomeSlot方法執行結束退出。

3. replaceStaleEntry
先來看replaceStaleEntry 方法,該方法源碼爲:

/*
 * @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).

	//向前找到第一個髒entry
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
1.          slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    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.
        if (k == key) {
			//如果在向後環形查找過程中發現key相同的entry就覆蓋並且和髒entry進行交換
2.            e.value = value;
3.            tab[i] = tab[staleSlot];
4.            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
			//如果在查找過程中還未發現髒entry,那麼就以當前位置作爲cleanSomeSlots
			//的起點
            if (slotToExpunge == staleSlot)
5.                slotToExpunge = i;
			//搜索髒entry並進行清理
6.            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.
		//如果向前未搜索到髒entry,則在查找過程遇到髒entry的話,後面就以此時這個位置
		//作爲起點執行cleanSomeSlots
        if (k == null && slotToExpunge == staleSlot)
7.            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
	//如果在查找過程中沒有找到可以覆蓋的entry,則將新的entry插入在髒entry
8.    tab[staleSlot].value = null;
9.    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
10.    if (slotToExpunge != staleSlot)
		//執行cleanSomeSlots
11.        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

該方法的邏輯請看註釋,下面我結合各種情況詳細說一下該方法的執行過程。首先先看這一部分的代碼:

int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

這部分代碼通過PreIndex方法實現往前環形搜索髒entry的功能,初始時slotToExpunge和staleSlot相同,若在搜索過程中發現了髒entry,則更新slotToExpunge爲當前索引i。另外,說明replaceStaleEntry並不僅僅侷限於處理當前已知的髒entry,它認爲在出現髒entry的相鄰位置也有很大概率出現髒entry,所以爲了一次處理到位,就需要向前環形搜索,找到前面的髒entry。那麼根據在向前搜索中是否還有髒entry以及在for循環後向環形查找中是否找到可覆蓋的entry,我們分這四種情況來充分理解這個方法:

1.前向有髒entry

  • 1.1後向環形查找找到可覆蓋的entry

該情形如下圖所示。
在這裏插入圖片描述
如圖,slotToExpunge初始狀態和staleSlot相同,當前向環形搜索遇到髒entry時,在第1行代碼中slotToExpunge會更新爲當前髒entry的索引i,直到遇到哈希桶(table[i])爲null的時候,前向搜索過程結束。在接下來的for循環中進行後向環形查找,若查找到了可覆蓋的entry,第2,3,4行代碼先覆蓋當前位置的entry,然後再與staleSlot位置上的髒entry進行交換。交換之後髒entry就更換到了i處,最後使用cleanSomeSlots方法從slotToExpunge爲起點開始進行清理髒entry的過程

  • 1.2後向環形查找未找到可覆蓋的entry
    該情形如下圖所示。
    前向環形搜索到髒entry,向後環形未搜索可覆蓋entry.png
    如圖,slotToExpunge初始狀態和staleSlot相同,當前向環形搜索遇到髒entry時,在第1行代碼中slotToExpunge會更新爲當前髒entry的索引i,直到遇到哈希桶(table[i])爲null的時候,前向搜索過程結束。在接下來的for循環中進行後向環形查找,若沒有查找到了可覆蓋的entry,哈希桶(table[i])爲null的時候,後向環形查找過程結束。那麼接下來在8,9行代碼中,將插入的新entry直接放在staleSlot處即可,最後使用cleanSomeSlots方法從slotToExpunge爲起點開始進行清理髒entry的過程

2.前向沒有髒entry

  • 2.1 後向環形查找找到可覆蓋的entry 該情形如下圖所示。
    在這裏插入圖片描述
    如圖,slotToExpunge初始狀態和staleSlot相同,當前向環形搜索直到遇到哈希桶(table[i])爲null的時候,前向搜索過程結束,若在整個過程未遇到髒entry,slotToExpunge初始狀態依舊和staleSlot相同。在接下來的for循環中進行後向環形查找,若遇到了髒entry,在第7行代碼中更新slotToExpunge爲位置i。若查找到了可覆蓋的entry,第2,3,4行代碼先覆蓋當前位置的entry,然後再與staleSlot位置上的髒entry進行交換,交換之後髒entry就更換到了i處。如果在整個查找過程中都還沒有遇到髒entry的話,會通過第5行代碼,將slotToExpunge更新當前i處,最後使用cleanSomeSlots方法從slotToExpunge爲起點開始進行清理髒entry的過程。

  • 2.2 後向環形查找未找到可覆蓋的entry 該情形如下圖所示。
    前向環形未搜索到髒entry,後向環形查找未查找到可覆蓋的entry.png
    如圖,slotToExpunge初始狀態和staleSlot相同,當前向環形搜索直到遇到哈希桶(table[i])爲null的時候,前向搜索過程結束,若在整個過程未遇到髒entry,slotToExpunge初始狀態依舊和staleSlot相同。在接下來的for循環中進行後向環形查找,若遇到了髒entry,在第7行代碼中更新slotToExpunge爲位置i。若沒有查找到了可覆蓋的entry,哈希桶(table[i])爲null的時候,後向環形查找過程結束。那麼接下來在8,9行代碼中,將插入的新entry直接放在staleSlot處即可。另外,如果發現slotToExpunge被重置,則第10行代碼if判斷爲true,就使用cleanSomeSlots方法從slotToExpunge爲起點開始進行清理髒entry的過程。

下面用一個實例來有個直觀的感受,示例代碼就不給出了,代碼debug時table狀態如下圖所示:
在這裏插入圖片描述
如圖所示,當前的staleSolt爲i=4,首先先進行前向搜索髒entry,當i=3的時候遇到髒entry,slotToExpung更新爲3,當i=2的時候tabel[2]爲null,因此前向搜索髒entry的過程結束。然後進行後向環形查找,知道i=7的時候遇到table[7]爲null,結束後向查找過程,並且在該過程並沒有找到可以覆蓋的entry。最後只能在staleSlot(4)處插入新entry,然後從slotToExpunge(3)爲起點進行cleanSomeSlots進行髒entry的清理。是不是上面的1.2的情況。

當我們調用threadLocal的get方法時,當table[i]不是和所要找的key相同的話,會繼續通過threadLocalMap的 getEntryAfterMiss方法向後環形去找,該方法爲:

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

當key==null的時候,即遇到髒entry也會調用expungeStleEntry對髒entry進行清理。

當我們調用threadLocal.remove方法時候,實際上會調用threadLocalMap的remove方法,該方法的源碼爲:

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

同樣的可以看出,當遇到了key爲null的髒entry的時候,也會調用expungeStaleEntry清理掉髒entry。

從以上set,getEntry,remove方法看出,在threadLocal的生命週期裏,針對threadLocal存在的內存泄漏的問題,都會通過expungeStaleEntry,cleanSomeSlots,replaceStaleEntry這三個方法清理掉key爲null的髒entry。

4. 爲什麼使用弱引用?
從文章開頭通過threadLocal,threadLocalMap,entry的引用關係看起來threadLocal存在內存泄漏的問題似乎是因爲threadLocal是被弱引用修飾的。那爲什麼要使用弱引用呢?

如果使用強引用

假設threadLocal使用的是強引用,在業務代碼中執行threadLocalInstance==null操作,以清理掉threadLocal實例的目的,但是因爲threadLocalMap的Entry強引用threadLocal,因此在gc的時候進行可達性分析,threadLocal依然可達,對threadLocal並不會進行垃圾回收,這樣就無法真正達到業務邏輯的目的,出現邏輯錯誤

如果使用弱引用

假設Entry弱引用threadLocal,儘管會出現內存泄漏的問題,但是在threadLocal的生命週期裏(set,getEntry,remove)裏,都會針對key爲null的髒entry進行處理。

從以上的分析可以看出,使用弱引用的話在threadLocal生命週期裏會盡可能的保證不出現內存泄漏的問題,達到安全的狀態。

5. Thread.exit()
當線程退出時會執行exit方法:

private void exit() {
    if (group != null) {
        group.threadTerminated(this);
        group = null;
    }
    /* Aggressively null out all reference fields: see bug 4006245 */
    target = null;
    /* Speed the release of some of these resources */
    threadLocals = null;
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    blocker = null;
    uncaughtExceptionHandler = null;
}

從源碼可以看出當線程結束時,會令threadLocals=null,也就意味着GC的時候就可以將threadLocalMap進行垃圾回收,換句話說threadLocalMap生命週期實際上thread的生命週期相同。

4. ThreadLocal的使用場景

ThreadLocal 不是用來解決共享對象的多線程訪問問題的,數據實質上是放在每個thread實例引用的threadLocalMap,也就是說每個不同的線程都擁有專屬於自己的數據容器(threadLocalMap),彼此不影響。因此threadLocal只適用於 共享對象會造成線程安全 的業務場景。比如hibernate中通過threadLocal管理Session就是一個典型的案例,不同的請求線程(用戶)擁有自己的session,若將session共享出去被多線程訪問,必然會帶來線程安全問題。下面,我們自己來寫一個例子,SimpleDateFormat.parse方法會有線程安全的問題,我們可以嘗試使用threadLocal包裝SimpleDateFormat,將該實例不被多線程共享即可。

public class ThreadLocalDemo {
    private static ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<>();
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100; i++) {
            executorService.submit(new DateUtil("2019-11-25 09:00:" + i % 60));
        }
    }

    static class DateUtil implements Runnable {
        private String date;

        public DateUtil(String date) {
            this.date = date;
        }

        @Override
        public void run() {
            if (sdf.get() == null) {
                sdf.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
            } else {
                try {
                    Date date = sdf.get().parse(this.date);
                    System.out.println(date);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

如果當前線程不持有SimpleDateformat對象實例,那麼就新建一個並把它設置到當前線程中,如果已經持有,就直接使用。另外,從 if (sdf.get() == null){…}else{…}可以看出爲每一個線程分配一個SimpleDateformat對象實例是從應用層面(業務代碼邏輯)去保證的。
在上面我們說過threadLocal有可能存在內存泄漏,在使用完之後,最好使用remove方法將這個變量移除,就像在使用數據庫連接一樣,及時關閉連接。

總結:

  1. 每次使用完ThreadLocal,都調用它的remove()方法,清除數據。
  2. 在使用線程池的情況下,沒有及時清理ThreadLocal,不僅是內存泄漏的問題,更嚴重的是可能導致業務邏輯出現問題。所以,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章