天天看點

【Java 線程系列】ThreadLocal進階解析

作者:​​半身風雪​​

上篇:​​Java 天生就是多線程​​

系列文章簡介:​

​上一節我們都明白了為什麼Java 天生就是多線程,這一節我們一起來學習ThreadLocal進階解析。​

【Java 線程系列】ThreadLocal進階解析

@​​TOC​​

一、ThreadLocal 辨析

        ThreadLocal 和 Synchonized 都用于解決多線程并發訪問。可是 ThreadLocal 與 synchronized 有本質的差别。synchronized 是利用鎖的機制,使變量或代碼塊 在某一時該僅僅能被一個線程通路。而 ThreadLocal 為每個線程都提供了變量的 副本,使得每個線程在某一時間訪問到的并非同一個對象,這樣就隔離了多個線 程對資料的資料共享。

二、ThreadLocal 的使用

ThreadLocal 類接口很簡單,隻有 4 個方法,我們先來了解一下:
  • ​void set(Object value)​

設定目前線程的線程局部變量的值。

  • ​public Object get()​

該方法傳回目前線程所對應的線程局部變量。

  • ​public void remove()​

将目前線程局部變量的值删除,目的是為了減少記憶體的占用,該方法是 JDK 5.0 新增的方法。需要指出的是,當線程結束後,對應該線程的局部變量将自動 被垃圾回收,是以顯式調用該方法清除線程的局部變量并不是必須的操作,但它 可以加快記憶體回收的速度。

  • ​protected Object initialValue()​

傳回該線程局部變量的初始值,該方法是一個 protected 的方法,顯然是為 了讓子類覆寫而設計的。這個方法是一個延遲調用方法,線上程第 1 次調用 get() 或 set(Object)時才執行,并且僅執行 1 次。ThreadLocal 中的預設實作直接傳回一 個 null

public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();

        RESOURCE代表一個能夠存放String類型的ThreadLocal對象。 此時不論什麼一個線程能夠并發通路這個變量,對它進行寫入、讀取操作,都是 線程安全的。

三、ThreadLocal 解析

先來看一下源碼:
【Java 線程系列】ThreadLocal進階解析
【Java 線程系列】ThreadLocal進階解析
【Java 線程系列】ThreadLocal進階解析

        上面先取到目前線程,然後調用 getMap 方法擷取對應的 ThreadLocalMap, ThreadLocalMap 是 ThreadLocal 的靜态内部類,然後 Thread 類中有一個這樣類型 成員,是以 getMap 是直接傳回 Thread 的成員。

看下 ThreadLocal 的内部類 ThreadLocalMap 源碼:
【Java 線程系列】ThreadLocal進階解析

        可以看到有個 Entry 内部靜态類,它繼承了 WeakReference,總之它記錄了 兩個資訊,一個是 ​

​ThreadLocal<?>​

​類型,一個是 Object 類型的值。getEntry 方法 則是擷取某個 ThreadLocal 對應的值,set 方法就是更新或指派相應的 ThreadLocal 對應的值。

        回顧我們的 get 方法,其實就是​

​拿到每個線程獨有的 ThreadLocalMap​

​ 然後再用 ThreadLocal 的目前執行個體,拿到 Map 中的相應的 Entry,然後就可 以拿到相應的值傳回出去。當然,如果 Map 為空,還會先進行 map 的建立,初始化等工作。

四、引發的記憶體洩漏分析

【Java 線程系列】ThreadLocal進階解析

        上圖中的 o,我們可以稱之為對象引用,而 new Object()我們可以稱之為在記憶體 中産生了一個對象執行個體。

        當寫下 o=null 時,隻是表示 o 不再指向堆中 object 的對象執行個體,不代表這 個對象執行個體不存在了。

4.1、 強引用

       強引用就是指在程式代碼之中普遍存在的,類似“​

​Object obj=new Object()​

​” 這類的引用,隻要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象執行個體。

4.2、 軟引用

       軟引用是用來描述一些還有用但并非必需的對象。對于軟引用關聯着的對象, 在系統将要發生記憶體溢出異常之前,将會把這些對象執行個體列進回收範圍之中進行 第二次回收。如果這次回收還沒有足夠的記憶體,才會抛出記憶體溢出異常。在 JDK 1.2 之後,提供了 SoftReference 類來實作軟引用。

4.3、 弱引用

       弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象執行個體隻能生存到下一次垃圾收集發生之前。當垃圾收集器工作時, 無論目前記憶體是否足夠,都會回收掉隻被弱引用關聯的對象執行個體。在 JDK 1.2 之 後,提供了 WeakReference 類來實作弱引用。

4.4、 虛引用

       虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象執行個體是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象執行個體。為一個對象設定虛引用關聯的唯一目的就是能在這個對象執行個體被收集器回收時收到一個系統通知。在 JDK 1.2 之後,提供了 PhantomReference 類來實作虛引用。

下面我們來寫一段代碼,展示記憶體洩漏的現象:
public class ThreadLocalOOM {

    private static final int TASK_LOOP_SIZE = 500;

    //    這裡建立了5個線程池,大小固定為5 個線程,不明白沒關系,關注我,後期會講解
    final static ThreadPoolExecutor poolExecutor = new
            ThreadPoolExecutor(5, 5, 1,
            TimeUnit.MINUTES, new LinkedBlockingDeque<>());

    static class LocalVariable {
        //        5M 大小的意思
        private byte[] a = new byte[1024 * 1024 * 5];
    }

    final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < TASK_LOOP_SIZE; i++) {
            poolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    new LocalVariable();
                    System.out.println("use local variable");
                }
            });
            
            Thread.sleep(100);
        }
    }
}      

運作上面代碼,并将堆記憶體大小設 置為-Xmx256m。

       可以看到記憶體的實際使用控制在 25M 左右:因為每個任務中會不斷 new 出 一個 5M 的數組,5*5=25M,這是很合理的。

【Java 線程系列】ThreadLocal進階解析
當我們啟用了 ThreadLocal 以後:
【Java 線程系列】ThreadLocal進階解析
【Java 線程系列】ThreadLocal進階解析

記憶體占用最高升至 150M,一般情況下穩定在 90M 左右,那麼加入一個 ThreadLocal 後,記憶體的占用真的會這麼多?

于是,我們加入一行代碼:
【Java 線程系列】ThreadLocal進階解析
再執行,看看記憶體情況:
【Java 線程系列】ThreadLocal進階解析

       可以看見最高峰的記憶體占用也在 25M 左右,完全和我們不加 ThreadLocal 表 現一樣。這就充分說明,确實發生了記憶體洩漏。

五、 分析

       根據我們前面對 ThreadLocal 的分析,我們可以知道每個 Thread 維護一個 ThreadLocalMap,這個映射表的 key 是 ThreadLocal 執行個體本身,value 是真正需 要存儲的 Object,也就是說 ThreadLocal 本身并不存儲值,它隻是作為一個 key 來讓線程從 ThreadLocalMap 擷取 value。仔細觀察 ThreadLocalMap,這個 map 是使用 ThreadLocal 的弱引用作為 Key 的,弱引用的對象在 GC 時會被回收。

是以使用了 ThreadLocal 後,引用鍊如圖所示:
【Java 線程系列】ThreadLocal進階解析

圖中的虛線表示弱引用。

  • 這樣,當把 threadlocal 變量置為 null 以後,沒有任何強引用指向 threadlocal 執行個體,是以 threadlocal 将會被 gc 回收。這樣一來,ThreadLocalMap 中就會出現 key 為 null 的 Entry,就沒有辦法通路這些 key 為 null 的 Entry 的 value,如果目前 線程再遲遲不結束的話,這些 key 為 null 的 Entry 的 value 就會一直存在一條強引用鍊:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而這塊 value 永 遠不會被通路到了,是以存在着記憶體洩露。
  • 隻有目前 thread 結束以後,current thread 就不會存在棧中,強引用斷開, Current Thread、Map value 将全部被 GC 回收。最好的做法是不在需要使用 ThreadLocal 變量後,都調用它的 remove()方法,清除資料。
  • 其實考察 ThreadLocal 的實作,我們可以看見,無論是 get()、set()在某些時候,調用了expungeStaleEntry方法用來清除 Entry 中 Key 為 null 的 Value,但是這是不及時的,也不是每次都會執行的,是以一些情況下還是會發生記憶體洩露。 隻有 remove()方法中顯式調用了 expungeStaleEntry 方法。
  • 從表面上看記憶體洩漏的根源在于使用了弱引用,但是另一個問題也同樣值得思考:為什麼使用弱引用而不是強引用?
下面我們分兩種情況讨論:
  1. key 使用強引用:引用 ThreadLocal 的對象被回收了,但是 ThreadLocalMap 還持有 ThreadLocal 的強引用,如果沒有手動删除,ThreadLocal 的對象執行個體不會 被回收,導緻 Entry 記憶體洩漏。
  2. key 使用弱引用:引用的 ThreadLocal 的對象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使沒有手動删除,ThreadLocal 的對象執行個體也會被 回收。value 在下一次 ThreadLocalMap 調用 set,get,remove 都有機會被回收。
比較兩種情況,我們可以發現:

       由于 ThreadLocalMap 的生命周期跟 Thread 一樣長,如果都沒有手動删除對應 key,都會導緻記憶體洩漏,但是使用弱引用可以多一層保障。

        是以,ThreadLocal 記憶體洩漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一樣長,如果沒有手動删除對應 key 就會導緻記憶體洩漏,而不是因為弱引 用。

總結

JVM 利用設定 ThreadLocalMap 的 Key 為弱引用,來避免記憶體洩露。 JVM 利用調用 remove、get、set方法的時候,回收弱引用。

繼續閱讀