溫馨提示×

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

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

Java?ThreadLocal類如何使用

發(fā)布時(shí)間:2022-07-06 10:47:37 來(lái)源:億速云 閱讀:182 作者:iii 欄目:開(kāi)發(fā)技術(shù)

今天小編給大家分享一下Java ThreadLocal類如何使用的相關(guān)知識(shí)點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識(shí),所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來(lái)了解一下吧。

    如圖所示: 

    Java?ThreadLocal類如何使用

    快速開(kāi)始

    接下來(lái)我們就先用一個(gè)簡(jiǎn)單的樣例給大家展示一下ThreadLocal的基本用法

    package cuit.pymjl.thradlocal;
    
    /**
     * @author Pymjl
     * @version 1.0
     * @date 2022/7/1 10:56
     **/
    public class MainTest {
        static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
        static void print(String str) {
            //打印當(dāng)前線程中本地內(nèi)存中本地變量的值
            System.out.println(str + " :" + threadLocal.get());
            //清除本地內(nèi)存中的本地變量
            threadLocal.remove();
        }
    
        public static void main(String[] args) {
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //設(shè)置線程1中本地變量的值
                    threadLocal.set("thread1 local variable");
                    //調(diào)用打印方法
                    print("thread1");
                    //打印本地變量
                    System.out.println("after remove : " + threadLocal.get());
                }
            });
    
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //設(shè)置線程1中本地變量的值
                    threadLocal.set("thread2 local variable");
                    //調(diào)用打印方法
                    print("thread2");
                    //打印本地變量
                    System.out.println("after remove : " + threadLocal.get());
                }
            });
    
            t1.start();
            t2.start();
        }
    }

    運(yùn)行結(jié)果如圖所示:

    Java?ThreadLocal類如何使用

    ThreadLocal的原理

    ThreadLocal相關(guān)類圖

    我們先來(lái)看一下ThreadLocal 相關(guān)類的類圖結(jié)構(gòu),如圖所示: 

    Java?ThreadLocal類如何使用

     由該圖可知, Thread 類中有一個(gè)threadLocals 和一個(gè)inheritableThreadLocals , 它們都是ThreadLocalMap 類型的變量, 而ThreadLocalMap 是一個(gè)定制化的Hashmap 。在默認(rèn)情況下, 每個(gè)線程中的這兩個(gè)變量都為null ,只有當(dāng)前線程第一次調(diào)用ThreadLocal 的set 或者get 方法時(shí)才會(huì)創(chuàng)建它們。其實(shí)每個(gè)線程的本地變量不是存放在ThreadLocal 實(shí)例里面,而是存放在調(diào)用線程的threadLocals 變量里面。也就是說(shuō), ThreadLocal 類型的本地變量存放在具體的線程內(nèi)存空間中。ThreadLocal 就是一個(gè)工具殼,它通過(guò)set 方法把value 值放入調(diào)用線程的threadLocals 里面并存放起來(lái), 當(dāng)調(diào)用線程調(diào)用它的get 方法時(shí),再?gòu)漠?dāng)前線程的threadLocals 變量里面將其拿出來(lái)使用。 如果調(diào)用線程一直不終止, 那么這個(gè)本地變量會(huì)一直存放在調(diào)用線程的threadLocals 變量里面,所以當(dāng)不需要使用本地變量時(shí)可以通過(guò)調(diào)用ThreadLocal 變量的remove 方法,從當(dāng)前線程的threadLocals 里面刪除該本地變量。另外, Thread 里面的threadLocals 為何被設(shè)計(jì)為map 結(jié)構(gòu)?很明顯是因?yàn)槊總€(gè)線程可以關(guān)聯(lián)多個(gè)ThreadLocal 變量。 接下來(lái)我們來(lái)看看ThreadLocal的set、get、以及remove的源碼

    set

        public void set(T value) {
            // 1.獲取當(dāng)前線程(調(diào)用者線程)
            Thread t = Thread.currentThread();
            // 2.以當(dāng)前線程作為key值,去查找對(duì)應(yīng)的線程變量,找到對(duì)應(yīng)的map
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                // 3.如果map不為null,則直接添加元素
                map.set(this, value);
            } else {
                // 4.否則就先創(chuàng)建map,再添加元素
                createMap(t, value);
            }
        }
        void createMap(Thread t, T firstValue) {
            /**
             * 這里是創(chuàng)建一個(gè)ThreadLocalMap,以當(dāng)前調(diào)用線程的實(shí)例對(duì)象為key,初始值為value
             * 然后放入當(dāng)前線程的Therad.threadLocals屬性里面
             */
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
        ThreadLocalMap getMap(Thread t) {
            //這里就是直接獲取調(diào)用線程的成員屬性threadlocals
            return t.threadLocals;
        }

    get

        public T get() {
            // 1.獲取當(dāng)前線程
            Thread t = Thread.currentThread();
            // 2.獲取當(dāng)前線程的threadlocals,即ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            // 3.如果map不為null,則直接返回對(duì)應(yīng)的值
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            // 4.否則,則進(jìn)行初始化
            return setInitialValue();
        }

    下面是setInitialValue的代碼

    private T setInitialValue() {
        //初始化屬性,其實(shí)就是null
        T value = initialValue();
        //獲取當(dāng)前線程
        Thread t = Thread.currentThread();
        //通過(guò)當(dāng)前線程獲取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //如果map不為null,則直接添加元素
        if (map != null) {
            map.set(this, value);
        } else {
            //否則就創(chuàng)建,然后將創(chuàng)建好的map放入當(dāng)前線程的屬性threadlocals
            createMap(t, value);
        }
            //將當(dāng)前ThreadLocal實(shí)例注冊(cè)進(jìn)TerminatingThreadLocal類里面
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

    這里我需要補(bǔ)充說(shuō)明一下TerminatingThreadLocal。這個(gè)類是jdk11新出的,jdk8中并沒(méi)有這個(gè)類,所以在網(wǎng)上很多源碼分析中并未看見(jiàn)這個(gè)類的相關(guān)說(shuō)明。 這個(gè)類我看了一下源碼,其作用應(yīng)該是避免ThreadLocal內(nèi)存泄露的問(wèn)題(感興趣的可以去看看源碼,若有錯(cuò)誤,還請(qǐng)指正)。 這是官方對(duì)其的解釋:

    /**
     * A thread-local variable that is notified when a thread terminates and
     * it has been initialized in the terminating thread (even if it was
     * initialized with a null value).
     * 一個(gè)線程局部變量,
     * 當(dāng)一個(gè)線程終止并且它已經(jīng)在終止線程中被初始化時(shí)被通知(即使它被初始化為一個(gè)空值)。
     */

    remove

         public void remove() {
             //如果當(dāng)前線程的threadLocals 變量不為空, 則刪除當(dāng)前線程中指定ThreadLocal 實(shí)例的本地變量。
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null) {
                 m.remove(this);
             }
         }

    小結(jié)

    在每個(gè)線程內(nèi)部都有一個(gè)名為threadLocals 的成員變量, 該變量的類型為Hash Map , 其中key 為我們定義的ThreadLocal 變量的this 引用, value 則為我們使用set 方法設(shè)置的值。每個(gè)線程的本地變量存放在線程自己的內(nèi)存變量threadLocals 中,如果當(dāng)前線程一直不消亡, 那么這些本地變量會(huì)一直存在, 所以可能會(huì)造成內(nèi)存溢出, 因此使用完畢后要記得調(diào)用ThreadLocal 的remove 方法刪除對(duì)應(yīng)線程的threadLocals 中的本地變量。

    ThreadLocal內(nèi)存泄露

    為什么會(huì)出現(xiàn)內(nèi)存泄漏?

    ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個(gè)ThreadLocal沒(méi)有外部強(qiáng)引用來(lái)引用它,那么系統(tǒng) GC 的時(shí)候,這個(gè)ThreadLocal勢(shì)必會(huì)被回收,這樣一來(lái),ThreadLocalMap中就會(huì)出現(xiàn)key為null的Entry,就沒(méi)有辦法訪問(wèn)這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠(yuǎn)無(wú)法回收,造成內(nèi)存泄漏。 其實(shí),ThreadLocalMap的設(shè)計(jì)中已經(jīng)考慮到這種情況,也加上了一些防護(hù)措施:在ThreadLocal的get(),set(),remove()的時(shí)候都會(huì)清除線程ThreadLocalMap里所有key為null的value。 但是這些被動(dòng)的預(yù)防措施并不能保證不會(huì)內(nèi)存泄漏:

    • 使用static的ThreadLocal,延長(zhǎng)了ThreadLocal的生命周期,可能導(dǎo)致的內(nèi)存泄漏

    • 分配使用了ThreadLocal又不再調(diào)用get(),set(),remove()方法,那么就會(huì)導(dǎo)致內(nèi)存泄漏

    為什么使用弱引用?

    既然我們都知道,使用了弱引用會(huì)造成ThreadLocalMap內(nèi)存泄漏,那么官方為什么依然使用弱引用而不是強(qiáng)引用呢?這就要從使用弱引用和強(qiáng)引用的區(qū)別來(lái)說(shuō)起了:

    • 如果使用強(qiáng)引用:我們知道,ThreadLocalMap的生命周期基本和Thread的生命周期一樣,當(dāng)前線程如果沒(méi)有終止,那么ThreadLocalMap始終不會(huì)被GC回收,而ThreadLocalMap持有對(duì)ThreadLocal的強(qiáng)引用,那么ThreadLocal也不會(huì)被回收,當(dāng)線程生命周期長(zhǎng),如果沒(méi)有手動(dòng)刪除,則會(huì)造成kv累積,從而導(dǎo)致OOM

    • 如果使用弱引用:弱引用中的對(duì)象具有很短的聲明周期,因?yàn)樵谙到y(tǒng)GC時(shí),只要發(fā)現(xiàn)弱引用,不管堆空間是否足夠,都會(huì)將對(duì)象進(jìn)行回收。而當(dāng)ThreadLocal的強(qiáng)引用被回收時(shí),ThreadLocalMap所持有的弱引用也會(huì)被回收,如果沒(méi)有手動(dòng)刪除kv,那么會(huì)造成value累積,也會(huì)導(dǎo)致OOM

    對(duì)比可知,使用弱引用至少可以保證不會(huì)因?yàn)閙ap的key累積從而導(dǎo)致OOM,而對(duì)應(yīng)的value可以通過(guò)remove,get,set方法在下一次調(diào)用時(shí)被清除??梢?jiàn),內(nèi)存泄露的根源不是弱引用,而是ThreadLocalMap的生命周期和Thread一樣長(zhǎng),造成累積導(dǎo)致的

    解決方法

    既然問(wèn)題的根源是value的累積造成OOM,那么我們對(duì)癥下藥,每次使用完ThreadLocal調(diào)用remove()方法清理掉就行了。

    以上就是“Java ThreadLocal類如何使用”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會(huì)為大家更新不同的知識(shí),如果還想學(xué)習(xí)更多的知識(shí),請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

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

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

    AI