對ThreadLocal的一些思考

左眼眸子發表於2018-08-08

##ThreadLocal

ThreadLocal類用來提供執行緒內部的區域性變數,不同的執行緒之間不會相互干擾,這種變數線上程的生命週期內起作用,減少同一個執行緒內多個函式或元件之間一些公共變數的傳遞的複雜度。說白了,ThreadLocal就是建立了能夠拿到以自己為key存在當前執行緒的內容,來達到對當前執行緒中的所有方法共享,減少了引數的多層傳遞。 這麼說可能不明顯,我來舉個例子吧:

@Service
public class CommonService {

    public void toWork(User user){
        bus(user);
    }

    public void bus(User user){
        metro(user);
    }

    public void metro(User user){
        System.out.println("到達公司");
    }
}
複製程式碼

比如一個介面叫工作,需要呼叫去工作的方法,而去工作不管是公交還是地鐵都需要個人資訊。傳統做法就是如上程式碼,將user資訊傳遞下去。這和ThreadLocal有什麼關係呢?下面來看:

@Service
public class CommonService {
    private ThreadLocal<User> local = new ThreadLocal<>();

    public void toWork(User user){
        local.set(user);
        bus();
    }

    public void bus(){
        User user = local.get();
        metro();
    }

    public void metro(){
        User user = local.get();
        System.out.println("到達公司");
    }
}
複製程式碼

這樣在該類中,user都是可以共享的,如果要跨類,就得將local移動到工具類中,給不同的類呼叫,我就不多加寫了。特別注意,一個執行緒對同一個threadlocal來說只能塞入一個value,再次塞入就會更新之前塞入的值。

到這裡就會有人問了,為什麼不將user定義成全部變數呢? 你想想,每個請求的人都不一樣,如何定義全域性變數。

那ThreadLocal又是如何做到讓一個local變數就set進去就能拿到不同使用者訪問時的使用者資訊呢? 這就得從原始碼來解釋了。

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();
 }
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
}
複製程式碼

每一個請求其實就是一個執行緒,不管是get還是set都是操作當前執行緒的threadLocals變數,也就是說和當前執行緒threadLocals起到了全域性變數的作用,但只對當前執行緒有用。 下面來詳細講講執行緒儲存的ThreadLocalMap吧:

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();
}
複製程式碼

每個ThreadLocal物件都有一個hash值threadLocalHashCode,每初始化一個ThreadLocal物件,hash值就增加一個固定的大小0x61c88647。

在插入過程中,根據ThreadLocal物件的hash值,定位到table中的位置i,過程如下: 1、如果當前位置是空的,那麼正好,就初始化一個Entry物件放在位置i上; 2、不巧,位置i已經有Entry物件了,如果這個Entry物件的key正好是即將設定的key,那麼重新設定Entry中的value; 3、很不巧,位置i的Entry物件,和即將設定的key沒關係,那麼只能找下一個空位置;

這樣的話,在get的時候,也會根據ThreadLocal物件的hash值,定位到table中的位置,然後判斷該位置Entry物件中的key是否和get的key一致,如果不一致,就判斷下一個位置

可以發現,set和get如果衝突嚴重的話,效率很低,因為ThreadLoalMap是Thread的一個屬性,所以即使在自己的程式碼中控制了設定的元素個數,但還是不能控制其它程式碼的行為。

這裡需要注意的是,ThreadLoalMap的Entry是繼承WeakReference,和HashMap很大的區別是,Entry中沒有next欄位,所以就不存在連結串列的情況了。

##記憶體洩漏 通過之前的分析已經知道,當使用ThreadLocal儲存一個value時,會在ThreadLocalMap中的陣列插入一個Entry物件,按理說key-value都應該以強引用儲存在Entry物件中,但在ThreadLocalMap的實現中,key被儲存到了WeakReference物件中。

這就導致了一個問題,ThreadLocal在沒有外部強引用時,發生GC時會被回收,如果建立ThreadLocal的執行緒一直持續執行,那麼這個Entry物件中的value就有可能一直得不到回收,發生記憶體洩露。

但是記憶體洩漏是否真的會發生呢? 可以檢視ThreadLocalMap的getEntry(),getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e),expungeStaleEntry(int staleSlot),程式碼就不貼了,首先從ThreadLocal的直接索引位置(通過ThreadLocal.threadLocalHashCode & (len-1)運算得到)獲取Entry e,如果e不為null並且key相同則返回e;如果e為null或者key不一致則向下一個位置查詢,如果下一個位置的key和當前需要查詢的key相等,則返回對應的Entry,否則,如果key值為null,則擦除該位置的Entry,否則繼續向下一個位置查詢。 在這個過程中遇到的key為null的Entry都會被擦除,那麼Entry內的value也就沒有強引用鏈,自然會被回收。仔細研究程式碼可以發現,set操作也有類似的思想,將key為null的這些Entry都刪除,防止記憶體洩露。 當然如果呼叫remove方法,肯定會刪除對應的Entry物件,最為保險。


##題外話: 其實shiro儲存subject資訊就是通過ThreadLocal來儲存,然後通過SecurityUtils.getSubject()給不同層級的方法可以拿到subject,不過shiro特別之處就是用了InheritableThreadLocal。為什麼用這個呢?

InheritableThreadLocal 可以讓使用者自行 new Thread 出來的執行緒可以獲取到 Subject,否則使用者還要額外想辦法怎麼獲取到這個 Subject。通俗的說就是,當你啟動多執行緒的時候,如果用ThreadLocal將拿不到主執行緒的subject,這個就是解決了這個問題。配置化用InheritableThreadLocal就很好用。

相關文章