­

ThreadLocal原理大解析

今天呢,和大家聊一下ThreadLocal

1. 是什麼?

JDK1.2提供的的一個執行緒綁定變數的類。

他的思想就是:給每一個使用到這個資源的執行緒都克隆一份,實現了不同執行緒使用不同的資源,且該資源之間相互獨立

2. 為什麼用?

思考一個場景:資料庫連接的時候,我們會創建一個Connection連接,讓不同的執行緒使用。這個時候就會出現多個執行緒爭搶同一個資源的情況。

這種多個執行緒爭搶同一個資源的情況,很常見,我們常用的解決辦法也就兩種:空間換時間,時間換空間

沒有辦法,魚與熊掌不可兼得也。就如我們的CAP理論,也是犧牲其中一項,保證其他兩項。

而針對上面的場景我們的解決辦法如下:

  • 空間換時間:為每一個執行緒創建一個連接。
    • 直接在執行緒工作中,創建一個連接。(重複程式碼太多)
    • 使用ThreadLocal,為每一個執行緒綁定一個連接。
  • 時間換空間:對當前資源加鎖,每一次僅僅存在一個執行緒可以使用這個連接。

通過ThreadLocal為每一個執行緒綁定一個指定類型的變數,相當於執行緒私有化

3. 怎麼用?

ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.get();
threadLocal.set(1);
threadLocal.remove();

沒錯,這四行程式碼已經把ThreadLocal的使用方法表現得明明白白。

  • getThreadLocal拿出一個當前執行緒所擁有得對象
  • set給當前執行緒綁定一個對象
  • remove將當前執行緒綁定的當前對象移除

記住在使用的以後,一定要remove,一定要remove,一定要remove

為什麼要remove。相信不少小夥伴聽到過ThreadLocal會導致記憶體泄漏問題。

沒錯,所以為了解決這種情況,所以你懂吧,用完就移除,別浪費空間(渣男欣慰)

看到這,腦袋上有好多問號出現了(小朋友你是否有很多問號?

為啥會引發記憶體泄漏?

為啥不remove就記憶體泄漏了

它是怎麼講對象和執行緒綁定的

為啥get的時候拿到的就是當前執行緒的而不是其他執行緒的

它怎麼實現的???

來吧,開淦,源碼來

4. 源碼解讀

先來說一個思路:如果我們自己寫一個ThreadLocal會咋寫?

執行緒綁定一個對象。這難道不是我們熟知的map映射?有了Map我們就可以以執行緒為Key,對象為value添加到一個集合中,然後各種get,set,remove操作,想怎麼玩就怎麼玩,搞定。😀

這個時候,有兄弟說了。你這思路不對啊,你這一個執行緒僅僅只能存放一個類型的變數,那我想存多個呢?

摸摸自己充盈的發量,你說出了一句至理名言:萬般問題,皆繫於源頭和結果之中。

從結果考慮,讓開發者自己搞執行緒私有(估計被會開發者罵死)

來吧,從源頭考慮。現在我們的需求是:執行緒可以綁定多個值,而不僅僅是一個。嗯,沒錯,兄弟們把你們的想法說出來。

讓執行緒自己維護一個Map,將這個ThreadLocal作為Key,對象作為Value不就搞定了

兄弟,牛掰旮旯四


此時,又有兄弟說了。按照你這樣的做法,將ThreadLocal扔到執行緒本身的的Map里,那豈不是這個ThreadLocal一直被執行緒對象引用,所以在執行緒銷毀之前都是可達的,都無法GC呀,有BUG???

好,問題。這樣想,既然由於執行緒和ThreadLocal對象存在引用,導致無法GC,那我將你和執行緒之間的引用搞成弱引用或者軟引用不就成了。一GC你就沒了。

啥,你不知道啥是弱引用和軟引用???

前面講過的東西,算啦再給你們複習一波。

JDK中存在四種類型引用,默認是強引用,也就是我們經常乾的事情。瘋狂new,new,new。這個時候創建的對象都是強引用。

  • 強引用。直接new
  • 軟引用。通過SoftReference創建,在記憶體空間不足的時候直接銷毀,即它可能最後的銷毀地點是在老年區
  • 弱引用。通過WeakReference創建,在GC的時候直接銷毀。即其銷毀地點必定為伊甸區
  • 虛引用。通過PhantomReference創建,它和不存也一樣,非常虛,只能通過引用隊列在進行一些操作,主要用於堆外記憶體回收

好了,回到正題,上面的引用里最適合我們當前的場景的就是弱引用了,為什麼這個樣子說:

在以往我們使用完對象以後等著GC清理,但是對於ThreadLocal來說,即使我們使用結束,也會因為執行緒本身存在該對象的引用,處於對象可達狀態,垃圾回收器無法回收。這個時候當ThreadLocal太多的時候就會出現記憶體泄漏的問題。

而我們將ThreadLocal對象的引用作為弱引用,那麼就很好的解決了這個問題。當我們自己使用完ThreadLocal以後,GC的時候就會將我們創建的強引用直接幹掉,而這個時候我們完全可以將執行緒Map中的引用幹掉,於是使用了弱引用,這個時候大家應該懂了為啥不使用軟引用了吧

還有一個問題:為什麼會引發記憶體泄漏呢?

了解Map結構的兄弟們應該清楚,內部實際就一個節點數組,對於ThreadLocalMap而言,內部是一個Entity,它將Key作為弱引用,Value還是強引用。如果我們在使用完ThreadLocal以後,沒有對Entity進行移除,會引發記憶體泄漏問題。

ThreadLocalMap提供了一個方法expungeStaleEntry方法用來排除無效的EntityKey為空的實體)

說到這裡,有一個問題我思考了蠻久的,value為啥不搞成弱引用,用完直接扔了多好

最後思考出來得答案(按照源碼推了一下):

不設置為弱引用,是因為不清楚這個Value除了map的引用還是否還存在其他引用,如果不存在其他引用,當GC的時候就會直接將這個Value幹掉了,而此時我們的ThreadLocal還處於使用期間,就會造成Value為null的錯誤,所以將其設置為強引用。

而為了解決這個強引用的問題,它提供了一種機制就是上面我們說的將KeyNullEntity直接清除

到這裡,這個類的設計已經很清楚了。接下來我們看一下源碼吧!


需要注意的一個點是:ThreadLocalMap解決哈希衝突的方式是線性探測法。

人話就是:如果當前數組位有值,則判斷下一個數組位是否有值,如果有值繼續向下尋找,直到一個為空的數組位

Set方法

class ThreadLocal	
	public void set(T value) {
    	//拿到當前執行緒
        Thread t = Thread.currentThread();
    //獲取當前執行緒的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //如果當前執行緒的Map已經創建,直接set
            map.set(this, value);
        else
            //沒有創建,則創建Map
            createMap(t, value);
    }

	private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
			//拿到當前數組位,當前數組位是否位null,如果為null,直接賦值,如果不為null,則線性查找一個null,賦值
            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;
        //清除一些失效的Entity
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }


	ThreadLocalMap getMap(Thread t) {
    //獲取當前執行緒的ThreadLocalMap
        return t.threadLocals;
    }

	void createMap(Thread t, T firstValue) {
        	//當前對象作為Key,和我們的設想一樣
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

Get方法

	public T get() {
        //獲取當前執行緒
        Thread t = Thread.currentThread();
        //拿到當前執行緒的Map
        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();
    }

	private Entry getEntry(ThreadLocal<?> key) {
        //計算數組位
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
        //如果當前數組有值,且數組位的key相同,則返回value
            if (e != null && e.get() == key)
                return e;
            else
                //線性探測尋找對應的Key
                return getEntryAfterMiss(key, i, e);
        }

	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)
                    //排除當前為空的Entity
                    expungeStaleEntry(i);
                else
                    //獲取下一個數組位
                    i = nextIndex(i, len);
                e = tab[i];
            }
        //如果沒有找到直接返回空
            return null;
        }

remove

	public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

	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();
                    //清空位NUll的實體
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

我們可以看到一個現象:在set,get,remove的時候都調用了expungeStaleEntry來將所有失效的Entity移除

看一下這個方法做了什麼

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // 刪除實體的Value
            tab[staleSlot].value = null;
    //置空這個數組位
            tab[staleSlot] = null;
    //數量減一
            size--;

            // 重新計算一次哈希,如果當前數組位不為null,線性查找直到一個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;
        }

更多原創內容請關注部落客公眾號@MakerStack