死磕ThreadLocal,為何ThreadLocal實現如此復雜,直接封裝HashMap不香嗎?


你以為你懂了,其實你沒有!  ——寫給自己

 

一直以來認為ThreadLocal只是簡單的分裝了一下HashMap,使用線程作為key來存儲。這樣也符合我們的習慣思維。需要存儲多少線程變量就創建多少ThreadLocal。

及如下圖這樣:

package gy.mao;
import java.util.HashMap;
import java.util.Map;

public class MyThreadLocal<T> {
    Map<Thread, T> map = null;
    public MyThreadLocal() {
    }

    public void set(T value) {
        if (map == null) {
            map = new HashMap<>();
        }
        map.put(Thread.currentThread(), value);
    }

    public T get(){
        if (map == null) {
            throw new RuntimeException("還未初始化");
        }
        return  map.get(Thread.currentThread());
    }

    public int size(){
        if (map == null) {
            throw new RuntimeException("還未初始化");
        }
        return map.size();
    }
}

及通過對HashMap實現簡單的封裝就直接使用;

然而今天仔細的看了看卻發現他實際是這樣的

 

問題一、為什么實際的ThreadLocal卻不是我們通常所理解的這樣呢?

這就涉及到我們常說的內存泄露的問題。

通過簡單封裝HashMap的方式,如果不手動remove()線程所放入的Entry。那么由於ThreadLocal一直存在,導致該Entry將一直持有,不會被垃圾回收器回收。隨着線程不斷的創建,put數據,必然會內存溢出。

 

問題二、可是不都說使用的ThreadLocal也是需要remove() 才行,要不然也會內存溢出啊

在回答這個問題前,先做個測試

首先是使用我自己實現的MyThreadLocal測試

然后是使用原生的ThreadLocal測試

兩個程序實現一樣的功能,都是不斷的創建線程然后往ThreadLocal中存數據。

可以很明顯的觀察到,使用我自己實現的MyThreadLocal最終會導致內存溢出,並且手動觸發垃圾回收也無濟於事;這也很符合我們的預期。

可是使用原生的ThreadLocal及時在沒有使用remove的情況下,也沒有內存溢出,而且垃圾回收還很規律的在進行。

 

問題三、ThreadLocal不使用remove()會有內存泄露的問題?

一起來簡單看下源碼,set()方法,

首先是調用了getMap方法及通過當前線程去獲取ThreadLocalMap(再深入可以看到該ThreadLocalMap實際是屬於當前線程所有),

沒有則創建,有則直接set值進去(注意此時的set的值為this,及當前ThreadLocal)。

再來看下ThreaLocalMap,可以很明顯的看到里面的Entry繼承了弱引用

ThreadLocal的真正的實現是這樣的(圖片來源於網絡)

 

 

 

因此可以看出,真正的ThreadLocal的哲學為:一個線程維護一個ThreaLocalMap,多個線程變量ThreadLocal存放在這個ThreadLocalMap中,ThreadLocal以弱引用的方式作為ThreadLocalMap的key。

所謂的不remove()會導致內存溢出是: 由於ThreadLocalMap的Entry中key是弱引用,當ThreadLocal不再被使用及會被垃圾回收器回收,但是value 由於是被Entry強引用,而Entry又是被ThreadLocalMap強引用,而ThreadLocalMap又是屬於當前線程的。這樣就形成key已經被回收了,但是Value一直不能回收,且不會再次使用到。

 

問題四、前面的實驗中使用ThreadLocal沒有導致內存泄露的原因是?

仔細看代碼就知道,代碼中每次都是創建線程之后就往ThreadLocal中放數據,而放完數據之后線程馬上就結束了。因此對應的ThreadLocal,依附於Thread的ThreadLocalMap都是可以被垃圾回收的,及皮將不存,毛之焉附。而不remove會到導致內存泄露是線程還在繼續運行的情況。

 

問題五、為啥ThreadLocalMap的key是弱引用而不是正常的強引用呢?

如果ThreadLocalMap的key為強引用,那么回收ThreadLocal時,因為ThreadLocalMap還持有ThreadLocal的強引用,ThreadLocal不會被回收,造成浪費,從某種意義上來說也是一種內存泄露。

 

此外,ThreadLocal沒有采用我鎖實現的MyThreadLocal這種實現思想的一個重要原因是鎖的問題。

眾所周知HashMap有線程安全的問題,如果要使用MyThreadLocal的這種思想必然得考慮該問題,得使用鎖來控制,這無疑增加了性能損耗和復雜度。

而反觀系統提供的ThreadLocal,由於是一個線程維護一個ThreadLocalMap數據,從設計上已經避免了多線程帶來的安全問題。

 

總結

由於Thread中包含變量ThreadLocalMap,因此ThreadLocalMap與Thread的生命周期是一樣長,如果都沒有手動刪除對應key,都會導致內存泄漏。

但是使用弱引用可以多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次ThreadLocalMap調用set(),get(),remove()的時候會被清除。

因此,ThreadLocal內存泄漏的根源是:由於ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因為弱引用。

ThreadLocal正確的使用方法

  • 每次使用完ThreadLocal都調用它的remove()方法清除數據
  • 將ThreadLocal變量定義成private static,這樣就一直存在ThreadLocal的強引用,也就能保證任何時候都能通過ThreadLocal的弱引用訪問到Entry的value值,進而清除掉 。

參考:https://zhuanlan.zhihu.com/p/102571059

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM