ThreadLocal 系列之 ThreadLocal 會內存泄漏嗎?

相關文章:

談到 ThreadLocal 在使用的時候的注意事項,其中有一個就是內存泄露,那麼 ThreadLocal 會發生內存泄漏嗎?或者說什麼情況下 ThreadLocal 會內存泄漏呢?

存入超大對象

無須多言,存入“超大對象”肯定會有內存泄漏的風險。

key 爲 null 造成內存泄漏

這裏引用一篇博客的觀點:

Threadlocal 裏面使用了一個存在弱引用的 Map,當釋放掉 Threadlocal 的強引用以後,Map裏面的 value 卻沒有被回收,而這塊 value 永遠不會被訪問到了, 所以存在着內存泄露。

https://my.oschina.net/u/658658/blog/1830812

很多文章都是這個觀點,主要說法是:由於 ThreadLocal.ThreadLocalMap.Entry 中的 key 是弱引用,會存在 keynull 的情況,這時候 value 無法被訪問到,但是由於 valueEntry 關聯,最主要原因是大部分情況下會使用線程池,造成 value 所佔內存無法被釋放,從而導致內存泄漏。

其實我們一般使用都會將 ThreadLocal 作爲一個類級別變量,會被強引用,這樣使用是不會出現"keynull 的情況"。

但是不可否認的是,如果將 ThreadLocal 作爲線程池中線程的局部變量的確會出現"keynull 的情況"。可以先看一個例子:

public class ThreadLocalOomDemo1 {

    private static final ExecutorService FIXED_EXECUTOR = Executors.newFixedThreadPool(1);

    public static void main(String[] args)  {

        FIXED_EXECUTOR.submit(() -> {
            ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
                @Override
                protected void finalize() {
                    System.out.println("finalize........");
                }
            };
            threadLocal.set("a");
            System.out.println(threadLocal.get());
        });
        System.out.println("-------------------");
        sleep(100);
        System.gc();
        FIXED_EXECUTOR.submit(()->{
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName());
        });
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出結果:

[0.002s][warning][gc] -XX:+PrintGC is deprecated. Will use -Xlog:gc instead.
[0.013s][info   ][gc] Using G1
-------------------
a
[0.296s][info   ][gc] GC(0) Pause Full (System.gc()) 5M->3M(17M) 4.705ms
finalize........
pool-1-thread-1

Debug 調試:
在這裏插入圖片描述
發現的確會存在出現 keynull ,已被 GC 的情況,此時 value 是無法獲取的。

ThreadLocalMap#set

ThreadLocal 中的數據本質是存儲到 ThreadLocalMap 中的,看下 set 方法:

 private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
						//出現 hash 衝突,使用開放地址法
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
								// key 相同,覆蓋
                if (k == key) {
                    e.value = value;
                    return;
                }
								//key 爲 null
                if (k == null) {
                  //進入替換方法
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

當出現 hash 衝突的時候,使用開放地址法向後查找,出現 keynull,通過replaceStaleEntry 方法處理。

ThreadLocalMap#get

同樣地,再看看 get 方法:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key) 
        return e;
    else
      //發生 hash 衝突
        return getEntryAfterMiss(key, i, e); 
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    //刪除過期數據
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

也就是說當出現 hash 衝突的時候,get 方法繼續查找,同樣會清除循環中遇到的過期 entry

總結

  • ThreadLocal 的確會出現內存溢出的情況,但是其實說 ThreadLocal 是否會出現內存泄漏,這個表述本身就有問題,真正應該關注的是在線程池中複用的 Thread 對象的成員變量是否會出現內存泄漏,
  • 不能直接以 key 出現 nullvalue 無法被訪問回收作爲緣由,我們平常都是將 ThreadLocal 作爲類級別變量,幾乎不可能會出現 keynull 的情況,即使出現了這樣的情況,ThreadLocal 在設計時 通過 getset 方法已經做了 keynull 的優化處理;
  • 使用完 ThreadLocal 後,都應該調用它的 remove 方法,避免可能出現的一段時間數據保留(如存入一個大對象,key GC 後,未出現 hash 衝突,那麼這個對象會有一段時間長期存在),也可以避免線程複用時出現髒數據;

References

  • https://www.jianshu.com/p/56f64e3c1b6c
  • https://www.iteye.com/blog/liuinsect-1827012

歡迎關注公衆號
​​​​​​在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章