ThreadLocal的弱引用 以及可能引起的內存泄漏

ThreadLocal:線程本地副本,可在多線程環境下,爲每個線程創建獨立的副本保證線程安全

ThreadLocal總會有一些疑惑的地方:

  1. 聽說ThreadLocal中有有使用弱引用,爲什麼要用弱引用?用弱引用,發生一次gc後,set進去的值再get就是null了嗎?
  2. 聽說ThreadLocal可能引起內存泄露?啥場景會內存泄露?爲何使用了弱引用依然可能發生內存泄露?怎麼避免?

解釋這兩個問題之前先看一下什麼是弱引用

Java中的弱引用具體指的是java.lang.ref.WeakReference<T>類,官方文檔說明是

弱引用對象的存在不會阻止它所指向的對象被垃圾回收器回收。弱引用最常見的用途是實現規範映射(canonicalizing mappings,比如哈希表)。假設垃圾收集器在某個時間點決定一個對象是弱可達的(weakly reachable)(也就是說當前指向它的全都是弱引用),這時垃圾收集器會清除所有指向該對象的弱引用,然後把這個弱可達對象標記爲可終結(finalizable)的,這樣它隨後就會被回收。與此同時或稍後,垃圾收集器會把那些剛清除的弱引用放入創建弱引用對象時所指定的引用隊列(Reference Queue)中。

 首先先來一段代碼,看下最基本的使用

public class TestThreadLocal {

    final static ThreadLocal<String> LOCAL = new ThreadLocal();

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        // 線程1
        executorService.execute(() -> {
            // 存值
            LOCAL.set(Thread.currentThread().getName());
            // 獲取值
            System.out.println(Thread.currentThread().getName() + "-->" +LOCAL.get());
        });
        // 線程2
        executorService.execute(() -> {
            LOCAL.set(Thread.currentThread().getName());
            System.out.println(Thread.currentThread().getName() + "-->" +LOCAL.get());
        });
        executorService.shutdown();
    }
}

 運行結果

pool-1-thread-1-->pool-1-thread-1
pool-1-thread-2-->pool-1-thread-2

結果沒有什麼懸念,現在我們就點源碼,看下它內部是怎麼存儲和獲取數據的(源碼基於jdk1.8,不同版本的jdk實現方式可能稍有不同)

首先看下ThreadLocal的set()

 

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

 

 

獲取當前線程,然後調用getMap(),返回的是線程下的ThreadLocalMap,ThreadLocalMap對象是Thread類中的成員變量,存放線程獨享的一些數據,簡單看一些ThreadLocalMap的實現

static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    ...
}

 

ThreadLocalMap是ThreadLocal類中獨立聲明的一個內部類,雖然它的名字叫Map,但並沒有實現Map接口,而是自己單獨實現的。同大多數的Map的實現類似,其內部也是維護了一個Entry存儲數據,Entry裏有key和value,其中的value維護在Entry裏,但是key卻並沒有直接在Entry裏聲明,看一看這個Entry,繼承WeakReference,泛型是ThreadLocal,是一個弱引用,繼續點進去WeakReference,WeakReference集成Reference,再點到Reference裏,發現一成員變量

我們再回到ThreadLocal的set()方法中,ThreadLocal的set,第四行上,實際上調用的是ThreadLocalMap的set(),點進去,繼續看map.set(this, value)的邏輯。

 

這就是個簡易版的Map的put存放數據的方法,相信大家都知道HashMap的實現,對此應該很清楚,存值的時候,沒有當前key,直接新增,有當前key,替換掉其value。但說明一點,與HashMap這種哈希鏈表存儲不同的是,在尋址衝突時,ThreadLocalMap並沒有使用鏈表或紅黑樹等方式鏈地址來解決,而是當前地址不可用,就在當前map的數據數組中繼續查找下一個可用的地址,有興趣的可以看下replaceStaleEntry(key, value, i)的具體實現,我們中的看一下新增key的邏輯,也就是tab[i] = new Entry(key, value);
點進去之後,又到了ThreadLocalMap的內部類Entry的構造方法中

 

構造方法第一行,也就是把key存放於Reference的成員變量中了;
兜了一圈,一句話總結這個ThreadLocal的set(T value),就是在當前線程的ThreadLocalMap裏存放了數據,key是使用弱引用的ThreadLocal,value就是我們set進去的value

ThreadLocal的獲取值等其他方法就不做過多分析了,下面重點分析下開始時拋出的問題一:關於弱引用的問題。
弱引用,在經歷一次gc後,不管當前內存是否足夠,都會被清除,我們把開始的代碼修改一下,看會不會被清除。爲了確認到底有沒有發生gc,在啓動時我們加入參數
-XX:+PrintGCDetails

 結果如下

可見ThreadLocal的使用沒有受到gc的影響,原因何在?
我們先分析一下里面的引用鏈,其中實現爲強引用,虛線爲弱引用 

可見,現在的ThreadLocal,是有兩條引用鏈的,一條是當前線程中的,另一條則是當前執行的測試類的成員變量,且爲強引用,所以並不會收到gc影響。

那麼我們爲什麼要用弱引用呢?正是因爲 只有弱引用指向的對象會被GC,可以充分利用內存空間,防止內存泄漏

然而,對於ThreadLocal,當線程池與其搭配使用時,使用不當仍會產生內存泄漏,我們來看一下代碼

 

我們再來看下問題二,內存泄露的問題,還是來段代碼跑跑再說,這段代碼,主要做的就是,開100個線程,每個線程都向ThreadLocal存入1M的對象,爲了儘快實驗出效果,我們把最大堆內存調小點
-Xmx50m -XX:+PrintGCDetails

public class TestThreadLocalLeak
{
   static ThreadLocal local = new ThreadLocal();
   final static ThreadLocal<byte[]> byteLocal = new ThreadLocal();
   final static int _1M = 1024 * 1024;

   public static void main(String[] args)
   {
      testUseThread();
      //    testUseThreadPool();
   }

   /**
    * 使用線程
    */
   private static void testUseThread()
   {
      for (int i = 0; i < 100; i++)
      {
         new Thread(() -> byteLocal.set(new byte[_1M])).start();
      }
   }

   /**
    * 使用線程池
    */
   private static void testUseThreadPool()
   {
      ExecutorService executorService = Executors.newFixedThreadPool(100);
      for (int i = 0; i < 100; i++)
      {
         executorService.execute(() -> byteLocal.set(new byte[_1M]));
      }
      executorService.shutdown();
   }

}

使用線程打印結果(部分日誌)

 

使用線程池打印結果(部分日誌)

 

 

當調用testUseThread()時,系統隨執行了大量YGC,內存始終穩定回收,最後正常執行,但是執行testUseThreadPool()時,經歷的頻繁的Full GC,內存卻沒有降下去,最終發生了OOM,原因就在於,使用Thread的時候,當線程執行完畢時,隨着線程的終止,該線程下的ThreadLocalMap此時是GC Root不可達的,同理,下面的Entry、裏面的key、value都會在下一次gc時被回收,而使用線程池後,由於線程使用後,不會被回收,自然ThreadLocalMap不會被回收引起內存泄露直至OOM。至於怎麼避免出現內存泄露,大家基本上也都知道,就是在使用完後,及時調用ThreadLocal的remove()即可,這個方法會把ThreadLocalMap中的相關key和value分別置爲null,就能在下次GC時回收了。


最後看一下弱引用的現象:

static ThreadLocal local = new ThreadLocal();

   public static void main(String[] args) {
      testWeakReference
   }
private static void testWeakReference(){
   local.set("測試ThreadLocalMap弱引用自動回收");
   Thread thread = Thread.currentThread();
   local = null;
   System.gc();
   System.out.println("");
}

在gc前和gc後打斷點,之前我們分析了,之所以ThreadLocal的數據不會被回收,是因爲有兩個引用鏈指向ThreadLocal,一個是當前線程的ThreadLocalMap,另一條就是當前類中的成員變量LOCAL,所以我們手動把LOCAL置爲null,再次調用System.gc(),看一下弱引用是不是被回收了
System.gc()前


參考資料:https://www.jianshu.com/p/94de80aee1bf

https://blog.csdn.net/qq_42862882/article/details/89820017

 

發佈了5 篇原創文章 · 獲贊 1 · 訪問量 8896
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章