当前位置:网站首页>9、聊聊ThreadLocal

9、聊聊ThreadLocal

2022-06-11 12:13:00 施小贊

1、ThreadLocal簡介

1.1、一些面試題

ThreadLocal中ThreadLocalMap的數據結構和關系?

ThreadLocal的key是弱引用,這是為什麼?

ThreadLocal內存泄露問題你知道嗎?

ThreadLocal中最後為什麼要加remove方法?

......

1.2、是什麼

稍微翻譯一下: 

ThreadLocal提供線程局部變量。這些變量與正常的變量不同,因為每一個線程在訪問ThreadLocal實例的時候(通過其get或set方法) 都有自己的、獨立初始化的變量副本 。ThreadLocal實例通常是類中的私有靜態字段,使用它的目的是希望將狀態(例如,用戶ID或事務ID)與線程關聯起來。

1.3、能幹嘛

 實現 每一個線程都有自己專屬的本地變量副本 (自己用自己的變量不麻煩別人,不和其他人共享,人人有份,人各一份), 

主要解决了讓每個線程綁定自己的值,通過使用get()和set()方法,獲取默認值或將其值更改為當前線程所存的副本的值從而避免了線程安全問題。 

1.4、api介紹

1.4、從helloworld講起

1.4.1、按照總銷售額統計,方便集團公司做計劃統計

群雄逐鹿起紛爭

package com.atguigu.juc.tl;





class MovieTicket

{

    int number = 50;



    public synchronized void saleTicket()

    {

        if(number >0)

        {

            System.out.println(Thread.currentThread().getName()+"\t"+"---賣出第: "+(number--));

        }else{

            System.out.println("----賣光了");

        }

    }

}





/**

 * @auther zzyy

 * @create 2021-03-23 15:03

 * 1  三個售票員賣完50張票務,總量完成即可,吃大鍋飯,售票員每個月固定月薪

 *

 * 2  分灶吃飯,各個銷售自己動手,豐衣足食

 */

public class ThreadLocalDemo

{

    public static void main(String[] args)

    {

        MovieTicket movieTicket = new MovieTicket();

        

        for (int i = 1; i <=3; i++) {

            new Thread(() -> {

                for (int j = 1; j <=20; j++) {

                    movieTicket.saleTicket();

                }

            },String.valueOf(i)).start();

        }



    }

}



運行結果:

1---賣出第: 50

1---賣出第: 49

1---賣出第: 48

。。。

1---賣出第: 33

1---賣出第: 32

1---賣出第: 31

2---賣出第: 30

2---賣出第: 29

。。。。

2---賣出第: 12

2---賣出第: 11

3---賣出第: 10

。。。

3---賣出第: 1

----賣光了

。。。

----賣光了

1.4.2、上述需求發生了變化...

不參加總和計算,希望各自分灶吃飯,各憑銷售本事提成,按照出單數各自統計

比如某找房軟件,每個中介銷售都有自己的銷售額指標,自己專屬自己的,不和別人摻和

人手一份天下安

package com.atguigu.juc.tl;



class House

{





    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public void saleHouse()

    {

        Integer value = threadLocal.get();

        ++value;

        threadLocal.set(value);

    }

    

}



public class ThreadLocalDemo

{

    public static void main(String[] args)

    {

        

        House house = new House();



        new Thread(() -> {

            try

            {

                for (int j = 1; j <=3; j++) {

                    house.saleHouse();

                }

                System.out.println(Thread.currentThread().getName()+"\t"+"---賣出: "+house.threadLocal.get());

            }catch (Exception e){

                e.printStackTrace();

            }finally {

                house.threadLocal.remove(); // 如果不清理自定義的  ThreadLocal  變量,可能會影響後續業務邏輯和造成內存泄露等問題

            }

        },"t1").start();



        new Thread(() -> {

            try

            {

                for (int j = 1; j <=2; j++) {

                    house.saleHouse();

                }

                System.out.println(Thread.currentThread().getName()+"\t"+"---賣出: "+house.threadLocal.get());

            }catch (Exception e){

                e.printStackTrace();

            }finally {

                house.threadLocal.remove();

            }

        },"t2").start();



        new Thread(() -> {

            try

            {

                for (int j = 1; j <=5; j++) {

                    house.saleHouse();

                }

                System.out.println(Thread.currentThread().getName()+"\t"+"---賣出: "+house.threadLocal.get());

            }catch (Exception e){

                e.printStackTrace();

            }finally {

                house.threadLocal.remove();

            }

        },"t3").start();

        

        System.out.println(Thread.currentThread().getName()+"\t"+"---賣出: "+house.threadLocal.get());

        

    }

}



運行結果:

t1---賣出: 3

t2---賣出: 2

main---賣出: 0

t3---賣出: 5

1.5、通過上面代碼總結

因為每個 Thread 內有自己的實例副本且該副本只由當前線程自己使用

既然其它 Thread 不可訪問,那就不存在多線程間共享的問題。

統一設置初始值,但是每個線程對這個值的修改都是各自線程互相獨立的

一句話:如何才能不爭搶

1 加入synchronized或者Lock控制資源的訪問順序

2 人手一份,大家各自安好,沒必要搶奪

2、從阿裏ThreadLocal規範開始

2.1、非線程安全的SimpleDateFormat

上述翻譯:SimpleDateFormat中的日期格式不是同步的。推薦(建議)為每個線程創建獨立的格式實例。如果多個線程同時訪問一個格式,則它必須保持外部同步。 

寫時間工具類,一般寫成 靜態的成員變量 ,不知,此種寫法的多線程下的危險性! 

課堂上討論一下SimpleDateFormat線程不安全問題,以及解决方法。  

package com.atguigu.juc.tl;



import java.text.SimpleDateFormat;

import java.util.Date;



/**

 * @author shizan

 * @Classname DateUtis

 * @Description TODO

 * @Date 2022/6/7 4:40 下午

 */

public class DateUtils {



    public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");



    /**

     * 模擬並發環境下使用 SimpleDateFormat 的 parse 方法將字符串轉換成 Date 對象

     * @param stringDate

     * @return

     * @throws Exception

     */

    public static Date parseDate(String stringDate) throws Exception {

        return sdf.parse(stringDate);

    }



    public static void main(String[] args) throws Exception {

        for (int i = 1; i <= 30; i++) {

            new Thread(() -> {

                try {

                    System.out.println(DateUtils.parseDate("2020-11-11 11:11:11"));

                } catch (Exception e) {

                    e.printStackTrace();

                }

            }, String.valueOf(i)).start();

        }

    }

}

源碼分析結論:

SimpleDateFormat類內部有一個Calendar對象引用,它用來儲存和這個SimpleDateFormat相關的日期信息,例如sdf.parse(dateStr),sdf.format(date) 諸如此類的方法參數傳入的日期相關String,Date等等, 都是交由Calendar引用來儲存的.這樣就會導致一個問題 如果你的SimpleDateFormat是個static的, 那麼多個thread 之間就會共享這個SimpleDateFormat, 同時也是共享這個Calendar引用。 

2.2、解决1

將SimpleDateFormat定義成局部變量。

缺點:每調用一次方法就會創建一個SimpleDateFormat對象,方法結束又要作為垃圾回收。

package com.atguigu.juc.tl;



import java.text.SimpleDateFormat;

import java.util.Date;



/**

 * @author shizan

 * @Classname DateUtis

 * @Description TODO

 * @Date 2022/6/7 4:40 下午

 */

public class DateUtils {





    public static void main(String[] args) throws Exception {

        for (int i = 1; i <= 30; i++) {

            new Thread(() -> {

                try {

                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

                    System.out.println(sdf.parse("2020-11-11 11:11:11"));

                    sdf = null;

                } catch (Exception e) {

                    e.printStackTrace();

                }

            }, String.valueOf(i)).start();

        }

    }

}

2.3、解决2

ThreadLocal,也叫做線程本地變量或者線程本地存儲

package com.atguigu.juc.tl;



import java.text.SimpleDateFormat;

import java.util.Date;



/**

 * @author shizan

 * @Classname DateUtis

 * @Description TODO

 * @Date 2022/6/7 4:40 下午

 */

public class DateUtils {

    private static final ThreadLocal<SimpleDateFormat> sdf_threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));



    /**

     * ThreadLocal 可以確保每個線程都可以得到各自單獨的一個 SimpleDateFormat 的對象,那麼自然也就不存在競爭問題了。

     *

     * @param stringDate

     * @return

     * @throws Exception

     */

    public static Date parseDateTL(String stringDate) throws Exception {

        return sdf_threadLocal.get().parse(stringDate);

    }



    public static void main(String[] args) throws Exception {

        for (int i = 1; i <= 30; i++) {

            new Thread(() -> {

                try {

                    System.out.println(DateUtils.parseDateTL("2020-11-11 11:11:11"));

                } catch (Exception e) {

                    e.printStackTrace();

                }

            }, String.valueOf(i)).start();

        }

    }

}

2.4、其它

① 加鎖

② 第3方時間庫

3、ThreadLocal源碼分析

3.1、Thread,ThreadLocal,ThreadLocalMap 關系

Thread和ThreadLocal

ThreadLocal和ThreadLocalMap

All三者總概括

threadLocalMap實際上就是一個以threadLocal實例為key,任意對象為value的Entry對象。

當我們為threadLocal變量賦值,實際上就是以當前threadLocal實例為key,值為value的Entry往這個threadLocalMap中存放 

3.2、小總結

近似的可以理解為 :   

ThreadLocalMap 從字面上就可以看出這是一個保存 ThreadLocal 對象的 map( 其實是以 ThreadLocal 為 Key) ,不過是經過了兩層包裝的 ThreadLocal 對象:   

JVM 內部維護了一個線程版的 Map<Thread,T>( 通過 ThreadLocal 對象的 set 方法,結果把 ThreadLocal 對象自己當做 key ,放進了 ThreadLoalMap 中 ) , 每個線程要用到這個 T 的時候,用當前的線程去 Map 裏面獲取, 通過這樣讓每個線程都擁有了自己獨立的變量 ,人手一份,競爭條件被徹底消除,在並發模式下是絕對安全的變量。   

4、ThreadLocal內存泄露問題

4.1、從阿裏面試題開始講起

4.2、什麼是內存泄漏

不再會被使用的對象或者變量占用的內存不能被回收,就是內存泄露。

4.3、誰惹的禍?

4.3.1、why?

4.3.2、强引用、軟引用、弱引用、虛引用分別是什麼?

4.3.2.1、再回首ThreadLocalMap

ThreadLocalMap  WeakReference    

ThreadLocalMap 從字面上就可以看出這是一個保存 ThreadLocal 對象的 map( 其實是以它為 Key) ,不過是經過了兩層包裝的 ThreadLocal 對象:   

( 1 )第一層包裝是使用 WeakReference<ThreadLocal<?>> 將 ThreadLocal 對象變成一個 弱引用的對象;   

( 2 )第二層包裝是定義了一個專門的類 Entry 來擴展 WeakReference<ThreadLocal<?>> ;   

4.3.2.2、整體架構

Java 技術允許使用 finalize() 方法在垃圾收集器將對象從內存中清除出去之前做必要的清理工作。   

4.3.2.3、强引用(默認支持模式) 

當內存不足,JVM開始垃圾回收,對於强引用的對象, 就算是出現了OOM也不會對該對象進行回收 , 死都不收。 

  

强引用是我們最常見的普通對象引用,只要還有强引用指向一個對象,就能錶明對象還“活著”,垃圾收集器不會碰這種對象。在 Java 中最常見的就是强引用,把一個對象賦給一個引用變量,這個引用變量就是一個强引用。當一個對象被强引用變量引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的, 即使該對象以後永遠都不會被用到JVM也不會回收 。因此强引用是造成Java內存泄漏的主要原因之一。 

  

對於一個普通的對象,如果沒有其他的引用關系,只要超過了引用的作用域或者顯式地將相應(强)引用賦值為 null, 

一般認為就是可以被垃圾收集的了(當然具體回收時機還是要看垃圾收集策略)。 

package com.atguigu.juc.tl;



import java.util.concurrent.TimeUnit;



/**

 * @author shizan

 * @Classname ReferenceDemoCopy

 * @Description TODO

 * @Date 2022/6/7 5:13 下午

 */

 class MyObject

{

    //一般這個方法工作中不用,此處為了講解gc,給學生們演示

    @Override

    protected void finalize() throws Throwable

    {

        System.out.println("------------- gc ,finalize() invoked");

    }

}

public class ReferenceDemoCopy {



    public static void main(String[] args) {

        MyObject myObject = new MyObject();//默認,强引用,死了都不放手

        System.out.println("gc before: "+myObject);



        myObject = null;

        System.gc();//手動擋的方式開啟Gc回收。

        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }



        System.out.println("gc after: "+myObject);

    }

}



運行結果:

gc before: [email protected]

------------- gc ,finalize() invoked

gc after: null

  

4.3.2.4、軟引用

軟引用是一種相對强引用弱化了一些的引用,需要用java.lang.ref.SoftReference類來實現,可以讓對象豁免一些垃圾收集。 

  

對於只有軟引用的對象來說, 

          當系統內存充足時它      不會     被回收, 

          當系統內存不足時它         會     被回收。 

軟引用通常用在對內存敏感的程序中,比如高速緩存就有用到軟引用, 內存够用的時候就保留,不够用就回收! 

package com.atguigu.juc.tl;



import java.lang.ref.SoftReference;

import java.util.concurrent.TimeUnit;



/**

 * @author shizan

 * @Classname ReferenceDemoCopy

 * @Description TODO

 * @Date 2022/6/7 5:13 下午

 */

class MyObject

{

    //一般這個方法工作中不用,此處為了講解gc,給學生們演示

    @Override

    protected void finalize() throws Throwable

    {

        System.out.println("------------- gc ,finalize() invoked");

    }

}

public class ReferenceDemoCopy {



    public static void main(String[] args) {



        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());//軟引用



        //當我們內存不够用的時候, soft 會被回收的情况,設置我們的內存大小: -Xms10m -Xmx10m

        System.out.println("gc before內存够用: "+softReference.get());



        System.gc();//手動擋的方式開啟Gc回收。

        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }





        //設置參數-Xms10m -Xmx10m

        System.out.println("gc before: "+softReference.get());



        try

        {

            byte[] bytes = new byte[9 * 1024 * 1024];

        }catch (Exception e){

            e.printStackTrace();

        }finally {

            System.out.println("-----gc after內存不够: "+softReference.get());

        }

    }

}



運行結果:



沒加-Xms10m -Xmx10m 參數,內存够用

gc before內存够用: [email protected]

gc before: [email protected]

-----gc after內存不够: [email protected]



加上 -Xms10m -Xmx10m參數,內存不够用

gc before內存够用: [email protected]

gc before: [email protected]

-----gc after內存不够: null

------------- gc ,finalize() invoked

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

at com.atguigu.juc.tl.ReferenceDemoCopy.main(ReferenceDemoCopy.java:31)

4.3.2.5、弱引用

弱引用需要用java.lang.ref.WeakReference類來實現,它比軟引用的生存期更短, 

對於只有弱引用的對象來說,只要垃圾回收機制一運行,不管JVM的內存空間是否足够,都會回收該對象占用的內存。  

package com.atguigu.juc.tl;



import java.lang.ref.SoftReference;

import java.lang.ref.WeakReference;

import java.util.concurrent.TimeUnit;



/**

 * @author shizan

 * @Classname ReferenceDemoCopy

 * @Description TODO

 * @Date 2022/6/7 5:13 下午

 */

class MyObject

{

    //一般這個方法工作中不用,此處為了講解gc,給學生們演示

    @Override

    protected void finalize() throws Throwable

    {

        System.out.println("------------- gc ,finalize() invoked");

    }

}

public class ReferenceDemoCopy {



    public static void main(String[] args) {

        WeakReference<MyObject> weakReference = new WeakReference(new MyObject());

        System.out.println("gc before: "+weakReference.get());



        System.gc();//手動擋的方式開啟Gc回收。

        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }



        System.out.println("gc after: "+weakReference.get());

    }

}

軟引用和弱引用的適用場景

假如有一個應用需要讀取大量的本地圖片:  

     *    如果每次讀取圖片都從硬盤讀取則會嚴重影響性能, 

     *    如果一次性全部加載到內存中又可能造成內存溢出。  

此時使用軟引用可以解决這個問題。 

  

  設計思路是:用一個HashMap來保存圖片的路徑和相應圖片對象關聯的軟引用之間的映射關系,在內存不足時,JVM會自動回收這些緩存圖片對象所占用的空間,從而有效地避免了OOM的問題。 

Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>(); 

4.3.2.6、虛引用

 虛引用需要java.lang.ref.PhantomReference類來實現。 

  

顧名思義, 就是形同虛設 ,與其他幾種引用都不同,虛引用並不會决定對象的生命周期。 

如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收 , 

它不能單獨使用也不能通過它訪問對象, 虛引用必須和引用隊列 (ReferenceQueue)聯合使用。 

  

虛引用的主要作用是跟踪對象被垃圾回收的狀態。  僅僅是提供了一種確保對象被 finalize以後,做某些事情的機制。  PhantomReference的get方法總是返回null ,因此無法訪問對應的引用對象。 

其意義在於:說明一個對象已經進入finalization階段,可以被gc回收,用來實現比finalization機制更靈活的回收操作 。 

換句話說,設置虛引用關聯的唯一目的,就是在這個對象被收集器回收的時候收到一個系統通知或者後續添加進一步的處理。

構造方法:

引用隊列

我被回收前需要被引用隊列保存下。 

package com.atguigu.juc.tl;



import java.lang.ref.*;

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.TimeUnit;



/**

 * @author shizan

 * @Classname ReferenceDemoCopy

 * @Description TODO

 * @Date 2022/6/7 5:13 下午

 */



class MyObject

{

    //一般這個方法工作中不用,此處為了講解gc,給學生們演示

    @Override

    protected void finalize() throws Throwable

    {

        System.out.println("------------- gc ,finalize() invoked");

    }

}

public class ReferenceDemoCopy {



    public static void main(String[] args) {



        ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<>();

        PhantomReference<MyObject> phantomReference = new PhantomReference<>(new MyObject(),referenceQueue);

        System.out.println(phantomReference.get());



        List<byte[]> list = new ArrayList<>();



        new Thread(() -> {

            while (true)

            {

                list.add(new byte[1 * 1024 * 1024]);

                try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }

                System.out.println(phantomReference.get());

            }

        },"t1").start();



        new Thread(() -> {

            while(true)

            {

                Reference<? extends MyObject> poll = referenceQueue.poll();

                if (poll != null) {

                    System.out.println("------有虛對象進入了隊列");

                }

            }

        },"t2").start();



        //暫停幾秒鐘線程

        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }





    }

}

4.3.2.7、GCRoots和四大引用小總結

4.3.3、關系

每個Thread對象維護著一個ThreadLocalMap的引用 

ThreadLocalMap是ThreadLocal的內部類,用Entry來進行存儲 

調用ThreadLocal的set()方法時,實際上就是往ThreadLocalMap設置值,key是ThreadLocal對象,值Value是傳遞進來的對象 

調用ThreadLocal的get()方法時,實際上就是往ThreadLocalMap獲取值,key是ThreadLocal對象 

ThreadLocal本身並不存儲值,它只是自己作為一個key來讓線程從ThreadLocalMap獲取value,正因為這個原理,所以ThreadLocal能够實現“數據隔離”,獲取當前線程的局部變量值,不受其他線程影響~ 

4.4、為什麼要用弱引用?不用如何?

public void function01() {

    ThreadLocal tl = new ThreadLocal<Integer>();  //line1

    tl.set(2021);  //line2

    tl.get();  //line3

}

line1新建了一個ThreadLocal對象,t1 是强引用指向這個對象;

line2調用set()方法後新建一個Entry,通過源碼可知Entry對象裏的k是弱引用指向這個對象。 

4.4.1、為什麼源代碼用弱引用?

當 function01 方法執行完畢後,棧幀銷毀强引用 tl 也就沒有了。但此時線程的 ThreadLocalMap 裏某個 entry 的 key 引用還指向這個對象   

若這個 key 引用是 强引用 ,就會導致 key 指向的 ThreadLocal 對象及 v 指向的對象不能被 gc 回收,造成內存泄漏;   

若這個 key 引用是 弱引用 就 大概率 會减少內存泄漏的問題 ( 還有一個 key 為 null 的雷 ) 。使用弱引用,就可以使 ThreadLocal 對象在方法執行完畢後順利被回收且 Entry 的 key 引用指向為 null 。 

-- 下面這句話,我們後續聊,本節先忽略 

此後我們調用 get,set  remove 方法時,就會嘗試删除 key  null  entry ,可以釋放 value 對象所占用的內存。   

4.4.2、弱引用就萬事大吉了嗎?

埋雷

1、 當我們為 threadLocal 變量賦值,實際上就是當前的 Entry(threadLocal 實例為 key ,值為 value) 往這個 threadLocalMap 中存放。 Entry 中的 key 是弱引用,當 threadLocal 外部强引用被置為 null(tl=null), 那麼系統 GC 的時候,根據可達性分析,這個 threadLocal 實例就沒有任何一條鏈路能够引用到它,這個 ThreadLocal 勢必會被回收,這樣一來 , ThreadLocalMap 中就會出現 key 為 null 的 Entry ,就沒有辦法訪問這些 key 為 null 的 Entry 的 value ,如果當前線程再遲遲不結束的話,這些 key 為 null 的 Entry 的 value 就會一直存在一條强引用鏈: Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永遠無法回收,造成內存泄漏。 

  

2、 當 然,如果當前 thread 運行結束, threadLocal , threadLocalMap,Entry 沒有引用鏈可達,在垃圾回收的時候都會被系統進行回收。   

   

3、 但在實際使用中 我們有時候會用線程池 去維護我們的線程,比如在 Executors.newFixedThreadPool() 時創建線程的時候,為了複用線程是不會結束的,所以 threadLocal 內存泄漏就值得我們小心   

key為null的entry,原理解析

ThreadLocalMap 使用 ThreadLocal 的弱引用作為 key ,如果一個 ThreadLocal 沒有外部强引用引用他,那麼系統 gc 的時候,這個 ThreadLocal 勢必會被回收,這樣一來, ThreadLocalMap 中就會出現 key 為 null 的 Entry ,就沒有辦法訪問這些 key 為 null 的 Entry 的 value ,如果當前線程再遲遲不結束的話 ( 比如正好用在線程池 ) ,這些 key 為 null 的 Entry 的 value 就會一直存在一條强引用鏈。   

  

雖然弱引用,保證了 key 指向的 ThreadLocal 對象能被及時回收,但是 v 指向的 value 對象是需要 ThreadLocalMap 調用 get 、 set 時發現 key 為 null 時才會去回收整個 entry 、 value , 因此弱引用不能 100% 保證內存不泄露。 我們要在不使用某個 ThreadLocal 對象後,手動調用 remove 方法來删除它 ,尤其是在線程池中,不僅僅是內存泄露的問題,因為線程池中的線程是重複使用的,意味著這個線程的 ThreadLocalMap 對象也是重複使用的,如果我們不手動調用 remove 方法,那麼後面的線程就有可能獲取到上個線程遺留下來的 value 值,造成 bug 。   

set、get方法會去檢查所有鍵為null的Entry對象

set()

get()

remove()

結論

從前面的set,getEntry,remove方法看出,在threadLocal的生命周期裏,針對threadLocal存在的內存泄漏的問題, 

都會通過expungeStaleEntry,cleanSomeSlots,replaceStaleEntry這三個方法清理掉key為null的髒entry。 

4.4.3、結論

4.5、最佳實踐

如何定義

用完記得手動remove

5、小總結

ThreadLocal 並不解决線程間共享數據的問題

ThreadLocal 適用於變量在線程間隔離且在方法間共享的場景

ThreadLocal 通過隱式的在不同線程內創建獨立實例副本避免了實例線程安全的問題

每個線程持有一個只屬於自己的專屬Map並維護了ThreadLocal對象與具體實例的映射,該Map由於只被持有它的線程訪問,故不存在線程安全以及鎖的問題

ThreadLocalMap的Entry對ThreadLocal的引用為弱引用,避免了ThreadLocal對象無法被回收的問題

都會通過expungeStaleEntry,cleanSomeSlots,replaceStaleEntry這三個方法回收鍵為 null 的 Entry 對象的值(即為具體實例)以及 Entry 對象本身從而防止內存泄漏,屬於安全加固的方法

群雄逐鹿起紛爭,人各一份天下安

原网站

版权声明
本文为[施小贊]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/162/202206111207398918.html