圖說(shuō)ThreadLocal

對(duì)ThreadLocal的實(shí)現(xiàn)一直比較模棱兩可,于是簡(jiǎn)單總結(jié)一下。若有錯(cuò)誤,歡迎指出,共同進(jìn)步~,如果您覺得有用,煩請(qǐng)點(diǎn)個(gè)贊哈。
個(gè)人認(rèn)為博客原文地址排版更有利于閱讀:圖說(shuō)ThreadLocal

眾所周知,ThreadLocal的設(shè)計(jì)原理是給每個(gè)線程保存一份獨(dú)立的變量的副本。這樣就可以避免因?yàn)槎鄠€(gè)線程對(duì)同一份資源(變量)的操作而產(chǎn)生一些并發(fā)問(wèn)題。相比于同步鎖來(lái)說(shuō),是空間換時(shí)間的做法。在許多框架中,應(yīng)用非常廣泛。比如Spring的Singleton作用域模式,以及Struts2的ActionContext等等。

簡(jiǎn)單使用樣例

要理解一樣?xùn)|西,首先要學(xué)會(huì)使用,還是老的思路,先寫個(gè)基本的使用的栗子。
這個(gè)栗子中,ThreadLocalDemo 擁有一個(gè)ThreadLocal類型的靜態(tài)變量local,然后在main方法中啟動(dòng)了兩個(gè)線程,這兩個(gè)線程分別對(duì)local中的數(shù)據(jù)(一個(gè)Integer類型的變量)進(jìn)行初始化,然后進(jìn)行加的操作,在操作的過(guò)程中使用了Thread.sleep(100L)增大兩個(gè)線程交叉執(zhí)行的幾率。

public class ThreadLocalDemo {
    private static ThreadLocal<Integer> local = new ThreadLocal<Integer>();

    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                local.set(0);
                Integer n = local.get();
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(100L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("線程r1:" + n++);
                    local.set(n);
                }
            }
        };
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                local.set(100);
                Integer n = local.get();
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(100L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("線程r2:" + n++);
                    local.set(n);
                }
            }
        };
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
    }

}

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

線程r1:0
線程r2:100
線程r1:1
線程r2:101
線程r1:2
線程r2:102
線程r1:3
線程r2:103
線程r1:4
線程r2:104
線程r1:5
線程r2:105
線程r1:6
線程r2:106
線程r1:7
線程r1:8
線程r2:107
線程r1:9
線程r2:108
線程r2:109

從結(jié)果看到,兩個(gè)線程的操作互不干擾,最后的輸出結(jié)果與各自線程單獨(dú)執(zhí)行的結(jié)果一致。這就是因?yàn)閮蓚€(gè)線程各自保存了這個(gè)變量的副本。此處如果在main方法中設(shè)置初始值(local.set(0))而不在各自的線程里設(shè)置,那么在r1和r2中取值時(shí)會(huì)報(bào)空指針異常,進(jìn)一步說(shuō)明了這個(gè)問(wèn)題:ThreadLocal中的變量是存在當(dāng)前線程中的。
本例中是將一個(gè)Integer存入到ThreadLocal中,如果我們將這個(gè)Integer換成一個(gè)map,那么我們就可以在一個(gè)ThreadLocal存放很多我們自己定義的數(shù)據(jù)了。

原理解析

宏觀關(guān)系分析

先上個(gè)圖就一目了然了,根據(jù)這個(gè)圖做一個(gè)簡(jiǎn)單的分析。

threadLocal結(jié)構(gòu)關(guān)系

圖中左邊是ThreadLocal類,右邊就是Thread類,也就是線程類。從宏觀上來(lái)說(shuō):
ThreadLocal中擁有一個(gè)內(nèi)部類ThreadLocalMap、一個(gè)set方法、一個(gè)get方法。
ThreadLocalMap是中是一個(gè)Entry結(jié)構(gòu)(鍵值對(duì),可以類比HashMap),這個(gè)Entry的key是一個(gè)ThreadLocal對(duì)象。
Thread類中聲明了一個(gè)ThreadLocalMap類型的變量。
ThreadLocal中的set方法、get方法實(shí)際上是在操作Thread類中的threadLocals。

源碼分析

ThreadLocal的set、get方法

set方法:

public void set(T value) {
    //獲取當(dāng)前線程的ThreadLocalMap,如果不為空
    //以當(dāng)前ThreadLocal對(duì)象為key,將當(dāng)前value存進(jìn)去
    //如果為空,就創(chuàng)建一個(gè)新的map
    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;
}
//ThreadLocalMap的set方法
private void set(ThreadLocal<?> key, Object value) {
    //創(chuàng)建Entry數(shù)組,計(jì)算hashcode值
    //如果hashcode對(duì)應(yīng)的位置已經(jīng)有Entry值,那么查看下一個(gè)位置,直到找到空的位置的下標(biāo)
    //這里有一個(gè)細(xì)節(jié),如果找到一個(gè)位置有Entry值,但是其Entry的key為null(說(shuō)明其ThreadLocal對(duì)象已經(jīng)被回收,后面會(huì)講到出現(xiàn)這種狀況的原因)
    //這個(gè)時(shí)候會(huì)調(diào)用replaceStaleEntry進(jìn)行一個(gè)替換的操作,將當(dāng)前key/value存入進(jìn)去
    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();
}

get方法:

public T get() {
    //獲取當(dāng)前線程的ThreadLocalMap
    //如果不為空,以當(dāng)前ThreadLocal對(duì)象為key,獲取value值
    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;
        }
    }
    //如果map為null,設(shè)置初始值
    return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
ThreadLocalMap

在set方法的時(shí)候,有一個(gè)檢查ThreadLocalMap中鍵值對(duì)的key為null,但是本身Entry對(duì)象不為null的細(xì)節(jié),為了明白這個(gè)細(xì)節(jié),最后我們看ThreadLocalMap的部分源碼:

static class ThreadLocalMap {

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

這里ThreadLocalMap中的Entry繼承了WeakReference,WeakReference是用來(lái)做什么的呢?
下面這段話摘自《深入理解Java虛擬機(jī)》:

無(wú)論是通過(guò)引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量, 還是通過(guò)可達(dá)性分析算法判斷對(duì)象的引用鏈?zhǔn)欠窨蛇_(dá), 判定對(duì)象是否存活都與“引用”有關(guān)。 在JDK 1.2以前, Java中的引用的定義很傳統(tǒng): 如果reference類型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址, 就稱這塊內(nèi)存代表著一個(gè)引用。 這種定義很純粹, 但是太過(guò)狹隘, 一個(gè)對(duì)象在這種定義下只有被引用或者沒有被引用兩種狀態(tài), 對(duì)于如何描述一些“食之無(wú)味, 棄之可惜”的對(duì)象就顯得無(wú)能為力。 我們希望能描述這樣一類對(duì)象: 當(dāng)內(nèi)存空間還足夠時(shí), 則能保留在內(nèi)存之中; 如果內(nèi)存空間在進(jìn)行垃圾收集后還是非常緊張, 則可以拋棄這些對(duì)象。 很多系統(tǒng)的緩存功能都符合這樣的應(yīng)用場(chǎng)景。在JDK 1.2之后, Java對(duì)引用的概念進(jìn)行了擴(kuò)充, 將引用分為強(qiáng)引用( StrongReference) 、 軟引用( Soft Reference) 、 弱引用( Weak Reference) 、 虛引用( PhantomReference) 4種, 這4種引用強(qiáng)度依次逐漸減弱。強(qiáng)引用就是指在程序代碼之中普遍存在的, 類似“Object obj=new Object( ) ”這類的引用, 只要強(qiáng)引用還存在, 垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。軟引用是用來(lái)描述一些還有用但并非必需的對(duì)象。 對(duì)于軟引用關(guān)聯(lián)著的對(duì)象, 在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前, 將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收。 如果這次回收還沒有足夠的內(nèi)存, 才會(huì)拋出內(nèi)存溢出異常。 在JDK 1.2之后, 提供了SoftReference類來(lái)實(shí)現(xiàn)軟引用。弱引用也是用來(lái)描述非必需對(duì)象的, 但是它的強(qiáng)度比軟引用更弱一些, 被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前。 當(dāng)垃圾收集器工作時(shí), 無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。 在JDK 1.2之后, 提供了WeakReference類來(lái)實(shí)現(xiàn)弱引用。虛引用也稱為幽靈引用或者幻影引用, 它是最弱的一種引用關(guān)系。 一個(gè)對(duì)象是否有虛引用的存在, 完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響, 也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例。 為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。 在JDK 1.2之后, 提供了PhantomReference類來(lái)實(shí)現(xiàn)虛引用。

通過(guò)這段話我們知道,WeakReference是用來(lái)將這個(gè)類聲明為弱引用的,一個(gè)類如果聲明為弱引用類型,那么這個(gè)類的實(shí)例會(huì)在下一次垃圾回收的時(shí)候被回收。那么這里的Entry為什么要特殊設(shè)置便于垃圾回收呢?
下圖是本文開頭的示例中ThreadLocal對(duì)象被引用的關(guān)系圖:

ThreadLocal引用關(guān)系

以本文開頭的用例來(lái)說(shuō),ThreadLocalDemo中有一個(gè)ThreadLocal對(duì)象的引用local,而在線程t1、t2中的ThreadLocalMap中的key也指向了這個(gè)ThreadLocal對(duì)象,如果我在main線程中不需要這個(gè)ThreadLocal對(duì)象了,我人為將靜態(tài)變量local指向?yàn)閚ull,那么這個(gè)ThreadLocal對(duì)象應(yīng)該被回收,因?yàn)閺?qiáng)引用已經(jīng)不在了。但是此時(shí)線程t1、t2中的ThreadLocalMap中的key還引用了這個(gè)ThreadLocal對(duì)象,如果這個(gè)引用是強(qiáng)引用,那么很顯然ThreadLocal對(duì)象將永遠(yuǎn)不會(huì)被回收,直到t1、t2線程終止,如果t1、t2是線程池中的線程,長(zhǎng)期不會(huì)被終止,那么這個(gè)ThreadLocal對(duì)象也就無(wú)法被回收,產(chǎn)生內(nèi)存泄漏。而弱引用就很好的解決了這個(gè)問(wèn)題,因?yàn)檫@個(gè)ThreadLocal對(duì)象的強(qiáng)引用一旦消失,只剩下幾個(gè)弱引用,那么下次垃圾回收的時(shí)候,這個(gè)ThreadLocal對(duì)象還是會(huì)被回收。所以就會(huì)出現(xiàn)上文set的時(shí)候,有的位置有Entry對(duì)象,但是卻沒有key的情況。實(shí)際上,由于弱引用的存在,導(dǎo)致ThreadLocal很多方法都需要去清理這種key為null的Entry。
參考:十分鐘理解Java中的弱引用ThreadLocal可能引起的內(nèi)存泄露

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容