ThreadLocal真的會造成內存泄漏嗎?
ThreadLoca在并發場景中,應用非常多。前幾天有位小伙伴問我一個問題,說ThreadLocal是不是真的會造成內存泄漏?今天給大家做一個分享。
1、ThreadLocal的基本原理
考慮到很多小伙伴可能還不太了解ThreadLocal,我先簡單介紹一下ThreadLocal。在多線程并發訪問同一個共享變量的情況下,如果不做同步控制的話,就可能會導致數據不一致的問題,所以,我們需要使用synchronized加鎖來解決。
而ThreadLocal換了一個思路來處理多線程的情況。
ThreadLocal本身并不存儲數據,它使用了線程中的threadLocals屬性,threadLocals的類型就是在ThreadLocal中的定義的ThreadLocalMap對象,當調用ThreadLocal的set(T value)方法時,ThreadLocal將自身的引用也就是this作為Key,然后,把用戶傳入的值作為Value存儲到線程的ThreadLocalMap中,這就相當于每個線程的讀寫操作都是基于線程自身的一個私有副本,線程之間的數據是相互隔離的,互不影響。
這樣一來基于ThreadLocal的操作也就不存在線程安全問題了。它相當于采用了用空間來換時間的思路,從而提高程序的執行效率。
2、四種對象引用
在ThreadLocalMap內部,維護了一個Entry數組table的屬性,用來存儲鍵值對的映射關系,來看這樣一段代碼片段:
static class ThreadLocalMap {
...
private Entry[] table;
static class Entry implements WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
Entry將ThreadLocal作為Key,值作為Value保存,它繼承自WeakReference,注意構造函數里的第一行代碼super(k),這意味著ThreadLocal對象是一個「弱引用」。有的小伙伴可能對「弱引用」不太熟悉,這里再介紹一下Java的四種引用關系。
在JDK1.2之后,Java對引用的概念做了一些擴充,將引用分為“強”、“軟”、“弱”、“虛”四種,由強到弱依次為:
強引用:指代碼中普遍存在的賦值行為,如:Object o = new Object(),只要強引用關系還在,對象就永遠不會被回收。
軟引用:還有用處,但不是必須存活的對象,JVM會在內存溢出前對其進行回收,例如:緩存。
弱引用:非必須存活的對象,引用關系比軟引用還弱,不管內存是否夠用,下次GC一定回收。
虛引用:也稱“幽靈引用”、“幻影引用”,最弱的引用關系,完全不影響對象的回收,等同于沒有引用,虛引用的唯一的目的是對象被回收時會收到一個系統通知。
這個描述還是比較官方的,簡單總結一下,大家應該都追過劇,強引用就好比是男主角,怎么都死不了。軟引用就像女主角,雖有一段經歷,還是沒走到最后。弱引用就是男二號,注定用來犧牲的。虛引用就是路人甲了。
3、造成內存泄漏的原因
內存泄漏和ThreadLocalMap中定義的Entry類有非常大的關系。
這個動畫完整地展示了ThreadLocal中對象引用的關系,需要這張高清圖的小伙伴可以在評論區留言。
由于ThreadLocal對象是弱引用,如果外部沒有強引用指向它,它就會被GC回收,導致Entry的Key為空(null),如果這時Value外部也沒有強引用指向它,那么Value就永遠也訪問不到了,按理也應該被GC回收,但是由于Entry對象還在強引用Value,導致Value無法被回收,這時「內存泄漏」就發生了,Value成了一個永遠也無法被訪問,但是又無法被回收的對象。
Entry對象屬于ThreadLocalMap,ThreadLocalMap又屬于Thread,如果線程本身的生命周期很短,短時間內就會被銷毀,那么「內存泄漏」立刻就會得到解決,只要線程被銷毀,Value也會隨之被回收。
問題是,線程本身是非常珍貴的計算機資源,很少會去頻繁的創建和銷毀,一般都是通過線程池來使用,這就將線程的生命周期大大拉長,「內存泄漏」的影響也會越來越大。
最后,一句話總結一下。
threadLocals對象中的Entry對象不再使用后,如果沒有及時清除Entry對象 ,而程序自身也無法通過垃圾回收機制自動清除,就可能導致內存泄漏。
4、如何避免內存泄漏?
不要聽到「內存泄漏」就不敢使用ThreadLocal,只要規范化使用是不會有問題的。我給大家支幾個招:
- 每次使用完ThreadLocal都記得調用remove()方法清除數據。
- 將ThreadLocal變量盡可能地定義成static final,避免頻繁創建ThreadLocal實例。這樣也就保證程序一直存在ThreadLocal的強引用,也能保證任何時候都能通過ThreadLocal的弱引用訪問到Entry的Value值,進而清除掉。
當然,就是使用不規范,ThreadLocal內部也做了一些優化,比如:
- 調用set()方法時,ThreadLocal會進行采樣清理、全量清理,擴容時還會繼續檢查。
- 調用get()方法時,如果沒有直接命中或者向后環形查找時也會進行清理。
- 調用remove()時,除了清理當前Entry,還會向后繼續清理。