Java ThreadLocal的內存泄漏問題

ThreadLocal提供了線程獨有的局部變量,可以在整個線程存活的過程中隨時取用,極大地方便了一些邏輯的實現。常見的ThreadLocal用法有:

- 存儲單個線程上下文信息。比如存儲id等;

- 使變量線程安全。變量既然成爲了每個線程內部的局部變量,自然就不會存在併發問題了;

- 減少參數傳遞。比如做一個trace工具,能夠輸出工程從開始到結束的整個一次處理過程中所有的信息,從而方便debug。由於需要在工程各處隨時取用,可放入ThreadLocal。

 

原理

ThreadLocal裏類型的變量,其實是放入了當前Thread裏。每個Thread都有一個{@link Thread#threadLocals},它是一個map:{@link java.lang.ThreadLocal.ThreadLocalMap}。這個map的entry是{@link java.lang.ThreadLocal.ThreadLocalMap.Entry},具體的key和value類型分別是{@link ThreadLocal}和 {@link Object}。(注:實際是ThreadLocal的弱引用WeakReference<ThreadLocal<?>>,但可以先簡單理解爲ThreadLocal。)

當設置一個ThreadLocal變量時,這個map裏就多了一對ThreadLocal -> Object的映射。

通過一個簡單程序來說明上圖:

package example.concurrency.tl;
 
/**
 * @author liuhaibo on 2018/05/23
 */
public class ThreadLocalDemo {
 
    private static final ThreadLocal<Integer> TL_INT = ThreadLocal.withInitial(() -> 6);
    private static final ThreadLocal<String> TL_STRING = ThreadLocal.withInitial(() -> "Hello, world");
 
    public static void main(String... args) {
        // 6
        System.out.println(TL_INT.get());
        TL_INT.set(TL_INT.get() + 1);
        // 7
        System.out.println(TL_INT.get());
        TL_INT.remove();
        // 會重新初始化該value,6
        System.out.println(TL_INT.get());
    }
}


-------------------------------
 | TL_INT    -> 6 |
 | TL_STRING -> "Hello, world"|

對於一個普通的map,取其中某個key對應的值分兩步:

1. 找到這個map;

2. 在map中,給出key,得到value。

 

想取出我們存放在當前線程裏的map裏的值同樣需要這兩步。但是,我們不需要告訴jvm map在哪兒,因爲jvm知道當前線程,也知道其局部變量map。所以最終的get操作只需要知道key就行了:int localInt = TL_INT.get();。

看起來有些奇怪,不同於常規的map的get操作的接口的樣子。

 

爲什麼key使用弱引用

不妨反過來想想,如果使用強引用,當ThreadLocal對象(假設爲ThreadLocal@123456)的引用(即:TL_INT,是一個強引用,指向ThreadLocal@123456)被回收了,ThreadLocalMap本身依然還持有ThreadLocal@123456的強引用,如果沒有手動刪除這個key,則ThreadLocal@123456不會被回收,所以只要當前線程不消亡,ThreadLocalMap引用的那些對象就不會被回收,可以認爲這導致Entry內存泄漏。

 

那使用弱引用的好處呢?

如果使用弱引用,那指向ThreadLocal@123456對象的引用就兩個:TL_INT強引用,和ThreadLocalMap中Entry的弱引用。一旦TL_INT被回收,則指向ThreadLocal@123456的就只有弱引用了,在下次gc的時候,這個ThreadLocal@123456就會被回收。

那麼問題來了,ThreadLocal@123456對象只是作爲ThreadLocalMap的一個key而存在的,現在它被回收了,但是它對應的value並沒有被回收,內存泄露依然存在!而且key被刪了之後,變成了null,value更是無法被訪問到了!針對這一問題,ThreadLocalMap類的設計本身已經有了這一問題的解決方案,那就是在每次get()/set()/remove()ThreadLocalMap中的值的時候,會自動清理key爲null的value。如此一來,value也能被回收了。

既然對key使用弱引用,能使key自動回收,那爲什麼不對value使用弱引用?答案顯而易見,假設往ThreadLocalMap裏存了一個value,gc過後value便消失了,那就無法使用ThreadLocalMap來達到存儲全線程變量的效果了。(但是再次訪問該key的時候,依然能取到value,此時取得的value是該value的初始值。即在刪除之後,如果再次訪問,取到null,會重新調用初始化方法。)

 

內存泄露

總結一下內存泄露(本該回收的無用對象沒有得到回收)的原因:

1 弱引用一定程度上回收了無用對象,但前提是開發者手動清理掉ThreadLocal對象的強引用(如TL_INT)。只要線程一直不死,ThreadLocalMap的key-value一直在漲。

解決方法:當某個ThreadLocal變量(比如:TL_INT)不再使用時,記得TL_INT.remove(),刪除該key。

2 在上例中,ThreadLocalDemo持有static的ThreadLocal類型:TL_INT,導致TL_INT的生命週期跟ThreadLocalDemo類的生命週期一樣長。意味着TL_INT不會被回收,弱引用形同虛設,所以當前線程無法通過ThreadLocalMap的防護措施清除TL_INT所對應的value(Integer)的強引用。通常,我們需要保證作爲key的TL_INT類型能夠被全局訪問到,同時也必須保證其爲單例,因此,在一個類中將其設爲static類型便成爲了慣用做法。殊不知這樣增加了ThreadLocal的使用風險。

ThreadLocal 最佳實踐

綜合上面的分析,我們可以理解ThreadLocal內存泄漏的前因後果,那麼怎麼避免內存泄漏呢?

每次使用完ThreadLocal,都調用它的remove()方法,清除數據。在使用線程池的情況下,沒有及時清理ThreadLocal,不僅是內存泄漏的問題,更嚴重的是可能導致業務邏輯出現問題。所以,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理。

 

線程池

使用了線程池,可以達到“線程複用”的效果。但是歸還線程之前記得清除ThreadLocalMap,要不然再取出該線程的時候,ThreadLocal變量還會存在。這就不僅僅是內存泄露的問題了,整個業務邏輯都可能會出錯。

 

解決方法參考:

/**
 * Method invoked upon completion of execution of the given Runnable.
 * This method is invoked by the thread that executed the task. If
 * non-null, the Throwable is the uncaught {@code RuntimeException}
 * or {@code Error} that caused execution to terminate abruptly.
 *
 * <p>This implementation does nothing, but may be customized in
 * subclasses. Note: To properly nest multiple overridings, subclasses
 * should generally invoke {@code super.afterExecute} at the
 * beginning of this method.
 *
... some deleted ...
 *
 * @param r the runnable that has completed
 * @param t the exception that caused termination, or null if
 * execution completed normally
 */
protected void afterExecute(Runnable r, Throwable t) { }
 
override {@link ThreadPoolExecutor#afterExecute(r, t)}方法,對ThreadLocalMap進行清理,比如:
 
protected void afterExecute(Runnable r, Throwable t) { 
    // you need to set this field via reflection.
    Thread.currentThread().threadLocals = null;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章