溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

ThreadLocal的原理是什么

發(fā)布時(shí)間:2021-07-24 09:54:01 來源:億速云 閱讀:119 作者:chen 欄目:編程語言

本篇內(nèi)容介紹了“ThreadLocal的原理是什么”的有關(guān)知識(shí),在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

1. 是什么?

JDK1.2提供的的一個(gè)線程綁定變量的類。

他的思想就是:給每一個(gè)使用到這個(gè)資源的線程都克隆一份,實(shí)現(xiàn)了不同線程使用不同的資源,且該資源之間相互獨(dú)立

2. 為什么用?

思考一個(gè)場景:數(shù)據(jù)庫連接的時(shí)候,我們會(huì)創(chuàng)建一個(gè)Connection連接,讓不同的線程使用。這個(gè)時(shí)候就會(huì)出現(xiàn)多個(gè)線程爭搶同一個(gè)資源的情況。

這種多個(gè)線程爭搶同一個(gè)資源的情況,很常見,我們常用的解決辦法也就兩種:空間換時(shí)間,時(shí)間換空間

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

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

  • 空間換時(shí)間:為每一個(gè)線程創(chuàng)建一個(gè)連接。

    • 直接在線程工作中,創(chuàng)建一個(gè)連接。(重復(fù)代碼太多)

    • 使用ThreadLocal,為每一個(gè)線程綁定一個(gè)連接。

  • 時(shí)間換空間:對(duì)當(dāng)前資源加鎖,每一次僅僅存在一個(gè)線程可以使用這個(gè)連接。

通過ThreadLocal為每一個(gè)線程綁定一個(gè)指定類型的變量,相當(dāng)于線程私有化

3. 怎么用?

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

沒錯(cuò),這四行代碼已經(jīng)把ThreadLocal的使用方法表現(xiàn)得明明白白。

  • getThreadLocal拿出一個(gè)當(dāng)前線程所擁有得對(duì)象

  • set給當(dāng)前線程綁定一個(gè)對(duì)象

  • remove將當(dāng)前線程綁定的當(dāng)前對(duì)象移除

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

為什么要remove。相信不少小伙伴聽到過ThreadLocal會(huì)導(dǎo)致內(nèi)存泄漏問題。

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

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

為啥會(huì)引發(fā)內(nèi)存泄漏?

為啥不remove就內(nèi)存泄漏了

它是怎么講對(duì)象和線程綁定的

為啥get的時(shí)候拿到的就是當(dāng)前線程的而不是其他線程的

它怎么實(shí)現(xiàn)的???

來吧,開淦,源碼來

4. 源碼解讀

先來說一個(gè)思路:如果我們自己寫一個(gè)ThreadLocal會(huì)咋寫?

線程綁定一個(gè)對(duì)象。**這難道不是我們熟知的map映射?**有了Map我們就可以以線程為Key,對(duì)象為value添加到一個(gè)集合中,然后各種get,set,remove操作,想怎么玩就怎么玩,搞定。????

這個(gè)時(shí)候,有兄弟說了。你這思路不對(duì)啊,你這一個(gè)線程僅僅只能存放一個(gè)類型的變量,那我想存多個(gè)呢?

摸摸自己充盈的發(fā)量,你說出了一句至理名言:萬般問題,皆系于源頭和結(jié)果之中。

從結(jié)果考慮,讓開發(fā)者自己搞線程私有(估計(jì)被會(huì)開發(fā)者罵死)

來吧,從源頭考慮?,F(xiàn)在我們的需求是:線程可以綁定多個(gè)值,而不僅僅是一個(gè)。嗯,沒錯(cuò),兄弟們把你們的想法說出來。

讓線程自己維護(hù)一個(gè)Map,將這個(gè)ThreadLocal作為Key,對(duì)象作為Value不就搞定了

兄弟,牛掰旮旯四


此時(shí),又有兄弟說了。按照你這樣的做法,將ThreadLocal扔到線程本身的的Map里,那豈不是這個(gè)ThreadLocal一直被線程對(duì)象引用,所以在線程銷毀之前都是可達(dá)的,都無法GC呀,有BUG???

**好,問題。**這樣想,既然由于線程和ThreadLocal對(duì)象存在引用,導(dǎo)致無法GC,那我將你和線程之間的引用搞成弱引用或者軟引用不就成了。一GC你就沒了。

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

前面講過的東西,算啦再給你們復(fù)習(xí)一波。

JDK中存在四種類型引用,默認(rèn)是強(qiáng)引用,也就是我們經(jīng)常干的事情。瘋狂new,new,new。這個(gè)時(shí)候創(chuàng)建的對(duì)象都是強(qiáng)引用。

  • 強(qiáng)引用。直接new

  • 軟引用。通過SoftReference創(chuàng)建,在內(nèi)存空間不足的時(shí)候直接銷毀,即它可能最后的銷毀地點(diǎn)是在老年區(qū)

  • 弱引用。通過WeakReference創(chuàng)建,在GC的時(shí)候直接銷毀。即其銷毀地點(diǎn)必定為伊甸區(qū)

  • 虛引用。通過PhantomReference創(chuàng)建,它和不存也一樣,非常虛,只能通過引用隊(duì)列在進(jìn)行一些操作,主要用于堆外內(nèi)存回收

好了,回到正題,上面的引用里最適合我們當(dāng)前的場景的就是弱引用了,為什么這個(gè)樣子說:

在以往我們使用完對(duì)象以后等著GC清理,但是對(duì)于ThreadLocal來說,即使我們使用結(jié)束,也會(huì)因?yàn)榫€程本身存在該對(duì)象的引用,處于對(duì)象可達(dá)狀態(tài),垃圾回收器無法回收。這個(gè)時(shí)候當(dāng)ThreadLocal太多的時(shí)候就會(huì)出現(xiàn)內(nèi)存泄漏的問題。

而我們將ThreadLocal對(duì)象的引用作為弱引用,那么就很好的解決了這個(gè)問題。當(dāng)我們自己使用完ThreadLocal以后,當(dāng)GC的時(shí)候就會(huì)將我們創(chuàng)建的強(qiáng)引用直接干掉,而這個(gè)時(shí)候我們完全可以將線程Map中的引用干掉,于是使用了弱引用,這個(gè)時(shí)候大家應(yīng)該懂了為啥不使用軟引用了吧

還有一個(gè)問題:為什么會(huì)引發(fā)內(nèi)存泄漏呢?

了解Map結(jié)構(gòu)的兄弟們應(yīng)該清楚,內(nèi)部實(shí)際就一個(gè)節(jié)點(diǎn)數(shù)組,對(duì)于ThreadLocalMap而言,內(nèi)部是一個(gè)Entity,它將Key作為弱引用,Value還是強(qiáng)引用。如果我們?cè)谑褂猛?code>ThreadLocal以后,沒有對(duì)Entity進(jìn)行移除,會(huì)引發(fā)內(nèi)存泄漏問題。

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

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

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

不設(shè)置為弱引用,是因?yàn)椴磺宄@個(gè)Value除了map的引用還是否還存在其他引用,如果不存在其他引用,當(dāng)GC的時(shí)候就會(huì)直接將這個(gè)Value干掉了,而此時(shí)我們的ThreadLocal還處于使用期間,就會(huì)造成Value為null的錯(cuò)誤,所以將其設(shè)置為強(qiáng)引用。

而為了解決這個(gè)強(qiáng)引用的問題,它提供了一種機(jī)制就是上面我們說的將KeyNullEntity直接清除

到這里,這個(gè)類的設(shè)計(jì)已經(jīng)很清楚了。接下來我們看一下源碼吧!


需要注意的一個(gè)點(diǎn)是:ThreadLocalMap解決哈希沖突的方式是線性探測法。

人話就是:如果當(dāng)前數(shù)組位有值,則判斷下一個(gè)數(shù)組位是否有值,如果有值繼續(xù)向下尋找,直到一個(gè)為空的數(shù)組位

Set方法

class ThreadLocal	
	public void set(T value) {
    	//拿到當(dāng)前線程
        Thread t = Thread.currentThread();
    //獲取當(dāng)前線程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //如果當(dāng)前線程的Map已經(jīng)創(chuàng)建,直接set
            map.set(this, value);
        else
            //沒有創(chuàng)建,則創(chuàng)建Map
            createMap(t, value);
    }

	private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
			//拿到當(dāng)前數(shù)組位,當(dāng)前數(shù)組位是否位null,如果為null,直接賦值,如果不為null,則線性查找一個(gè)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) {
    //獲取當(dāng)前線程的ThreadLocalMap
        return t.threadLocals;
    }

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

Get方法

	public T get() {
        //獲取當(dāng)前線程
        Thread t = Thread.currentThread();
        //拿到當(dāng)前線程的Map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //獲取這個(gè)實(shí)體
            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) {
        //計(jì)算數(shù)組位
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
        //如果當(dāng)前數(shù)組有值,且數(shù)組位的key相同,則返回value
            if (e != null && e.get() == key)
                return e;
            else
                //線性探測尋找對(duì)應(yīng)的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)
                    //排除當(dāng)前為空的Entity
                    expungeStaleEntry(i);
                else
                    //獲取下一個(gè)數(shù)組位
                    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);
        //拿到當(dāng)前的數(shù)組,判斷是否為需要的數(shù)組位,如果不是線性查找
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    //清空位NUll的實(shí)體
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

我們可以看到一個(gè)現(xiàn)象:在set,get,remove的時(shí)候都調(diào)用了expungeStaleEntry來將所有失效的Entity移除

看一下這個(gè)方法做了什么

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

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

            // 重新計(jì)算一次哈希,如果當(dāng)前數(shù)組位不為null,線性查找直到一個(gè)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;
        }

“ThreadLocal的原理是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI