ThreadLocal真會記憶體泄漏?

来源:https://www.cnblogs.com/lvlaotou/p/18128108
-Advertisement-
Play Games

Java的記憶體管理來說,就是ThreadLocal存在無法被GC回收的記憶體。這些無法被回收的記憶體,如果隨著時間的推移,從而導致超出記憶體容量「記憶體溢出」,最終導致程式崩潰「OutOfMemoryError」。所以為了避免我們的Java程式崩潰,我們必須要避免出現記憶體泄漏的問題。 ...


前言

在討論ThreadLocal存在記憶體泄漏問題之前,需要先瞭解下麵幾個知識點:

  • 什麼是記憶體泄漏?
  • 什麼是ThreadLocal?
  • 為什麼需要ThreadLocal?
    • 數據一致性問題
    • 如何解決數據一致性問題?

當我們瞭解了上面的知識點以後,會帶大家一起去瞭解真相。包括下麵幾個知識點:

  • 為什麼會產生記憶體泄漏?
  • 實戰復現問題
  • 如何解決記憶體泄漏?
  • 為什麼是弱引用?

只有瞭解上面的知識點,才能更好的理解以及如何解決ThreadLocal記憶體泄漏問題。下麵我們就開始帶大家一步一步的去瞭解。

什麼是記憶體泄漏?

在討論ThreadLocal存在記憶體泄漏問題之前,我覺得有必要先瞭解一下什麼是記憶體泄漏?我們為什麼要解決記憶體泄漏的問題?這裡引用一段百度百科對記憶體泄漏的解釋。

記憶體泄漏(Memory Leak)是指程式中已動態分配的堆記憶體由於某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式運行速度減慢甚至系統崩潰等嚴重後果。

從Java的記憶體管理來說,就是ThreadLocal存在無法被GC回收的記憶體。這些無法被回收的記憶體,如果隨著時間的推移,從而導致超出記憶體容量「記憶體溢出」,最終導致程式崩潰「OutOfMemoryError」。所以為了避免我們的Java程式崩潰,我們必須要避免出現記憶體泄漏的問題。

ThreadLocal

前面講了什麼是記憶體泄漏,為什麼要解決記憶體泄漏的問題。現在我們來講講什麼是ThreadLocal?

簡單來說,ThreadLocal是一個本地線程副本變數工具類。ThreadLocal讓每個線程有自己”獨立“的變數,線程之間互不影響。ThreadLocal為每個線程都創建一個副本,每個線程可以訪問自己內部的副本變數。

為什麼需要ThreadLocal?

現在我們知道了什麼是ThreadLocal,接下來我們講講為什麼需要ThreadLocal在講為什麼需要ThreadLocal之前,我們需要瞭解一個問題。那就是數據一致性問題。因為ThreadLocal就是解決數據一致性問題的一種方案,只要當我們瞭解什麼是數據一致性問題後,自然就知道為什麼需要ThreadLocal了。

什麼是一致性問題?

多線程充分利用了多核CPU的能力,為我們程式提供了很高的性能。但是有時候,我們需要多個線程互相協作,這裡可能就會涉及到數據一致性的問題。 數據一致性問題指的是:發生在多個主體對同一份數據無法達成共識。

如何解決一致性問題?

  • 排隊:如果兩個人對一個問題的看法不一致,那就排成一隊,一個人一個人去修改它,這樣後面一個人總是能夠得到前面一個人修改後的值,數據也就總是一致的了。Java中的互斥鎖等概念,就是利用了排隊的思想。排隊雖然能夠很好的確保數據一致性,但性能非常低。
  • 投票:,投票的話,多個人可以同時去做一件決策,或者同時去修改數據,但最終誰修改成功,是用投票來決定的。這個方式很高效,但它也會產生很多問題,比如網路中斷、欺詐等等。想要通過投票達到一致性非常複雜,往往需要嚴格的數學理論來證明,還需要中間有一些“信使”不斷來來回回傳遞消息,這中間也會有一些性能的開銷。我們在分散式系統中常見的Paxos和Raft演算法,就是使用投票來解決一致性問題的。
  • 避免:既然保證數據一致性很難,那我能不能通 過一些手段,去避免多個線程之間產生一致性問題呢?我們熟悉的Git就是這個實現,大家在本地分散式修改同一個文件,通過版本控制和解決衝突去解決這個問題。而ThreadLocal也是使用的這種方式。

為什麼會產生記憶體泄漏?

上面講清楚了ThreadLocal的基本含義,接下來我們一起看看ThreadLocal常用函數的源碼,只有瞭解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);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

從上面的源碼可以看出,當我們調用ThreadLocal對象的set()方法時,其實就是將ThreadLocal對象存入當前線程的ThreadLocalMap集合中,map集合的key為當前ThreadLocal對象,value為set()方法的參數。

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {

        Object value;

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

 private Entry[] table;
}

這是ThreadLocalMap的源碼(由於篇幅原因這裡我只取了重要的代碼),可以看到ThreadLocalMap中使用一個Entry對象來存儲數據,而Entry的key則是一個WeakReference弱引用對象。這裡我帶大家再複習一下Java對象的幾種引用。

  • 強引用:java中的引用預設就是強引用,任何一個對象的賦值操作就產生了對這個對象的強引用。如:Object o = new Object(),只要強引用關係還在,對象就永遠不會被回收。
  • 軟引用:java.lang.ref.SoftReference,JVM會在記憶體溢出前對其進行回收。
  • 弱引用:java.lang.ref.WeakReference,不管記憶體是否夠用,下次GC一定回收。
  • 虛引用:java.lang.ref.PhantomReference,也稱“幽靈引用”、“幻影引用”。虛作用是跟蹤垃圾回收器收集對象的活動,在GC的過程中,如果發現有PhantomReference,GC則會將引用放到ReferenceQueue中,由程式員自己處理,當程式員調用ReferenceQueue.pull()方法,將引用出ReferenceQueue移除之後,Reference對象會變成Inactive狀態,意味著被引用的對象可以被回收了,虛引用的唯一的目的是對象被回收時會收到一個系統通知。

實戰復現問題

上面我們已經瞭解了ThreadLocal存儲數據的set()方法,現在我們來看一段代碼,通過代碼來分析ThreadLocal為什麼會產生記憶體泄漏。

public class Test {

    @Override
    protected void finalize() throws Throwable {
        System.err.println("對象被回收了");
    }
}
@Test
void test() throws InterruptedException {
    ThreadLocal<Test> local = new ThreadLocal<>();
    local.set(new Test());
    local = null;
    System.gc();
    Thread.sleep(1000000);
}

我們創建一個測試類,並重寫finalize()方法,當對象被回收時會列印消息在控制台方便我們測試觀察對象是否被回收。

從代碼可以看到,我們創建了一個ThreadLocal對象,然後往對象裡面設置了一個new Test對象,然後我們將變數local賦值為null,最後手動觸發一下gc。大家可以猜猜,控制台會列印出對象被回收了的消息嗎?建議大家動手試試,增加一下理解。

在告訴大家答案之前我們先來分析一下上面的一個引用關係:

Untitled

示例中local = null這行代碼會將強引用2斷掉,這樣new ThreadLocal對象就只有一個弱引用4了,根據弱引用的特點在下次GC的時候new ThreadLocal對象就會被回收。那麼new Test對象就成了一個永遠無法訪問的對象,但是又存在一條強引用鏈thread→Thread對象→ThreadLocalMap→Entry→new Test,如果這條引用鏈一直存在就會導致new Test對象永遠不會被回收。因為現在大多時候都是使用線程池,而線程池會復用線程,就很容易導致引用鏈一直存在,從而導致new Test對象無法被回收,一旦這樣的情況隨著時間的推移而大量存在就容易引發記憶體泄漏。

如何解決記憶體泄漏?

我們已經知道了造成記憶體泄漏的原因,那麼要解決問題就很簡單了。

上面造成記憶體泄漏的第一點就是Entry的key也就是new ThreadLocal對象的強引用被斷開了,我們就可以想辦法讓這條強引用無法斷開,比如將ThreadLocal對象設置為private static 保證任何時候都能訪問new ThreadLocal對象同時避免其他地方將其賦值為null。

還有一種辦法就是想辦法將new Test對象回收,從根本上解決問題。下麵我們一起看看ThreadLocal為我們提供的方法。

remove()方法

public void remove() {
   ThreadLocalMap m = getMap(Thread.currentThread());
   if (m != null)
       m.remove(this);
}
private void remove(ThreadLocal<?> key) {
  Entry[] tab = table;
  int len = tab.length;
  int i = key.threadLocalHashCode & (len-1);
  for (Entry e = tab[i];
       e != null;
       e = tab[i = nextIndex(i, len)]) {
      if (e.get() == key) {
          e.clear();
          expungeStaleEntry(i);
          return;
      }
  }
}
private int expungeStaleEntry(int staleSlot) {
      Entry[] tab = table;
      int len = tab.length;

      // expunge entry at staleSlot
      tab[staleSlot].value = null;
      tab[staleSlot] = null;
      size--;
   // 省略代碼...感興趣可以去看看源碼
      return i;
  }

該方法的邏輯是,將entry里value的強引用3和key的弱引用4置為null。這樣new Test對象和Entry對象就都能被GC回收。

因此,只要調用了expungeStaleEntry() 就能將無用 Entry 回收清除掉。

但是該方法為private故無法直接調用,但是ThreadLocalMap中remove()方法直接調用了該方法,因此只要當我們使完ThreadLocal對象後調用一下remove()方法就能避免出現記憶體泄漏了。

綜上所述:針對ThreadLocal 記憶體泄露的原因,我們可以從兩方面去考慮:

  1. 刪除無用 Entry 對象。即 用完ThreadLocal後手動調用remove()方法。
  2. 可以讓ThreadLocal對象的強引用一直存在,保證任何時候都可以訪問到 Entry的 value值。即 將ThreadLocal 變數定義為 private static。

為什麼是弱引用?

不知道大家有沒有想過一個問題,既然是弱引用導致的記憶體泄漏,那麼為什麼JDK還要使用弱引用。難道是bug嗎?大家再看一下下麵這段代碼。

@Test
void test() throws InterruptedException {
    ThreadLocal<Test> local = new ThreadLocal<>();
    local.set(new Test());
    local = null;
    System.gc();
    Thread.sleep(1000000);
}

我們假設Entrykey使用強引用,那麼引用圖就是如下

Untitled

當代碼local = null斷掉強引用2的時候,new ThreadLocal對象就是只存在一條強引用4,那麼由於強引用的關係GC無法回收new ThreadLocal對象。所以就造成了Entry的key和value都無法訪問無法回收了,記憶體泄漏就加倍了。

同理也不能將Entry的value設置為弱引用,因為Entry對象的value即new Test對象只有一個引用,如果使用弱引用,在GC的時候會導致new Test對象被回收,導致數據丟失。

將Entry的key設置為弱引用還有一個好處就是,當強引用2斷掉且弱引用4被GC回收後,ThreadLocal會通過key.get() == null識別出無用Entry從而將Entry的key和value置為null以便被GC回收。具體代碼如下

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;
}

所以,Entry key使用弱引用並不是一個bug,而是ThreadLocal的開發人員在儘力的幫助我們避免造成記憶體泄漏。

彩蛋

@Test
void test2() throws InterruptedException {
    ThreadLocal<Test> local = new ThreadLocal<>();
  local.set(new Test());
    local = null;
    System.gc();
   for (int i = 0; i < 9; i++) {
        new ThreadLocal<>().get();
    }
    System.gc();
    Thread.sleep(1000000);
}

感興趣的同學可以嘗試運行上面的代碼,你會發現驚喜的!至於結果大家自己動手去獲取吧!。下麵我們再來看一個ThreadLocal常用的方法。

get()方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

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
        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;
}

從上面的代碼你會驚奇的發現,get方法也會調用expungeStaleEntry()方法,當然不是每次get都會調用。邏輯大家可以去看源碼慢慢理。這裡再提一下,可以順便看看完整的set方法,你還會發現秘密。

本文使用

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • XSL(eXtensible Stylesheet Language)是一種用於 XML 的樣式語言。 XSL(T) 語言 XSLT 是一種用於轉換 XML 文檔的語言。 XPath 是一種用於在 XML 文檔中導航的語言。 XQuery 是一種用於查詢 XML 文檔的語言。 它始於 XSL XSL ...
  • 一、前言 演算法(Algorithm)是指用來操作數據、解決程式問題的一組方法。對於同一個問題,使用不同的演算法,也許最終得到的結果是一樣的,但在過程中消耗的資源和時間卻會有很大的區別 衡量不同演算法之間的優劣主要是通過時間和空間兩個維度去考量: 時間維度:是指執行當前演算法所消耗的時間,我們通常用「時間復 ...
  • vue3 快速入門系列 - vue3 路由 在vue3 基礎上加入路由。 vue3 需要使用 vue-router V4,相對於 v3,大部分的 Vue Router API 都沒有變化。 Tip:不瞭解路由的同學可以看一下筆者之前的文章:vue2 路由 參考:vue2 路由官網、vue3 路由官網 ...
  • 引言:隨著深度學習技術的發展進步,已經不再依賴強大的GPU算力,便可實現AI推理了,讓AI技術滲透到了電腦、手機、智能設備等各類設備。體育、健身行業也不例外,阿裡體育等IT大廠,推出的樂動力、天天跳繩、百分運動等AI運動APP,讓雲上運動會、線上運動會、健身打卡、AI體育指導、AI體測等概念空前火熱 ...
  • 前言 首先這篇文章只是初步的嘗試,不涉及過於高深的編程技巧;同時需要表明的是,面向對象只是一種思想,不局限於什麼樣的編程語言,不可否認的是基於面向對象特性而設計的語言確實要比面向過程式的語言更加容易進行抽象和統籌,可以說面向對象的設計模式可以很大程度上擺脫過程的實例,但要論完整的應用來講,設計模式也 ...
  • 淘寶詳情API介面是用於獲取淘寶商品詳細信息的介面,它允許開發者通過發送請求,獲取商品的描述、價格、評價等信息。下麵是一個關於淘寶詳情API介面的示例文檔,包括介面地址、請求參數、響應參數等內容。 淘寶詳情API介面文檔 一、介面地址 https://api-gw.onebound.cn/taoba ...
  • 大家好,我是R哥。 Nacos 2.3.2 前幾天正式發佈了,修複了一個重大 bug。 Nacos 先掃個盲: Nacos 一個用於構建雲原生應用的動態服務發現、配置管理和服務管理平臺,由阿裡巴巴開源,致力於發現、配置和管理微服務。 說白了,Nacos 就是充當微服務中的的註冊中心和配置中心。 推薦 ...
  • 拓展閱讀 MySQL View MySQL truncate table 與 delete 清空表的區別和坑 MySQL Ruler mysql 日常開發規範 MySQL datetime timestamp 以及如何自動更新,如何實現範圍查詢 MySQL 06 mysql 如何實現類似 oracl ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...