字節面試:ThreadLocal內存泄漏,怎麼破?什麼是 ITL、TTL、FTL?

文章很長,且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 爲您奉上珍貴的學習資源 :

免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 :《尼恩技術聖經+高併發系列PDF》 ,幫你 實現技術自由,完成職業升級, 薪酬猛漲!加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領

免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取


字節面試:ThreadLocal內存泄漏,怎麼破?什麼是 ITL、TTL、FTL?

尼恩特別說明: 尼恩的文章,都會在 《技術自由圈》 公號 發佈, 並且維護最新版本。 如果發現圖片 不可見, 請去 《技術自由圈》 公號 查找

尼恩說在前面

在40歲老架構師 尼恩的讀者交流羣(50+)中,最近有小夥伴拿到了一線互聯網企業如得物、阿里、滴滴、極兔、有贊、希音、百度、網易、美團的面試資格,遇到很多很重要的面試題:

1.請解釋ThreadLocal是什麼,以及它的主要用途是什麼?

  1. ThreadLocal的內部機制是怎樣的?請解釋一下ThreadLocalMap和Entry。

3.使用ThreadLocal是否會導致內存泄漏?如果是,如何避免?

4.在使用線程池時,ThreadLocal可能會出現什麼問題?如何解決?

5.能否解釋一下 TransmittableThreadLocal 與ThreadLocal的區別和聯繫?

6.在父子線程間如何共享數據?ThreadLocal能實現嗎?如果不能,那應如何實現?

最近有小夥伴在面試字節,又遇到了ThreadLocal 相關的面試題。小夥伴懵了,支支吾吾的說了幾句,面試官不滿意,面試掛了。

所以,尼恩給大家做一下系統化、體系化的梳理,使得大家內力猛增,可以充分展示一下大家雄厚的 “技術肌肉”,讓面試官愛到 “不能自已、口水直流”,然後實現”offer直提”。

這裏,尼恩團隊把 ThreadLocal、ITL、TTL、FTL進行了穿透式的梳理,

梳理爲一個PDF文檔 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》, 並且持續迭代。

這個文檔將成爲大家 面試的殺手鐗, 此文當最新PDF版本,可以找40歲老架構師尼恩獲取。

當然,這道面試題,以及參考答案,也會收入咱們的 《尼恩Java面試寶典PDF》V171版本,供後面的小夥伴參考,提升大家的 3高 架構、設計、開發水平。

最新《尼恩 架構筆記》《尼恩高併發三部曲》《尼恩Java面試寶典》的PDF,請關注本公衆號【技術自由圈】獲取,回覆:領電子書

本文作者:

  • 第一作者 Moen (負責寫初稿 )
  • 第二作者 尼恩 (40歲老架構師, 負責提升此文的 技術高度,讓大家有一種 俯視 技術的感覺)

本文目錄

什麼是ThreadLocal(TL)?

在Java的多線程併發執行過程中,爲保證多個線程對變量的安全訪問,可以將變量放到ThreadLocal類型的對象中,使變量在每個線程中都有獨立值,不會出現一個線程讀取變量時而被另一個線程修改的現象。

ThreadLocal類通常被翻譯爲“線程本地變量” ,或者“線程局部變量” 。

ThreadLocal的英文字面翻譯爲“線程本地”,實質上ThreadLocal代表的是線程本地變量,可能將其命名爲ThreadLocalVariable會更加容易讓人理解。

以下來至官網的解釋

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

簡單翻譯如下:

此類提供線程局部變量。這些變量與其正常對應變量的不同之處在於,每個訪問一個變量(通過其get或set方法)的線程都有自己的獨立初始化的變量副本。ThreadLocal實例通常是類中的私有靜態字段,這些字段希望將狀態與線程(例如,用戶ID或事務ID)相關聯。

總結重點如下

  1. ThreadLocal 提供了一種訪問某個變量的特殊方式:訪問到的變量屬於當前線程,即保證每個線程的變量不一樣,而同一個線程在任何地方拿到的變量都是當前這個線程私有的,這就是所謂的線程隔離。

  2. 如果要使用 ThreadLocal,通常定義爲 private static 類型,根據編程範式最好是定義爲 private static final 類型。

ThreadLocal的基本使用

ThreadLocal是位於JDK的java.lang核心包中。

如果程序創建了一個ThreadLocal實例,那麼在訪問這個變量的值時,每個線程都會擁有一個獨立的自己的本地值

線程本地變量” 可以看成專屬於線程的變量,不受其他線程干擾,保存着線程的專屬數據。

當線程結束後,每個線程所擁有的那一個本地值也會被釋放。

在多線程併發操作“線程本地變量”時候,線程各自操作的是自己的本地值,從而規避了線程安全問題。

ThreadLocal如何是做到爲每個線程存有一份獨立的本地值的呢?

一個ThreadLocal實例可以形象地理解爲一個Map(早期版本的ThreadLocal是這樣設計的)。當工作線程Thread實例向本地變量保持某個值時,會以“Key-Value對”的形式保存在ThreadLocal內部的Map中,其中Key爲線程Thread實例Value爲待保存的值。當工作線程Thread實例從ThreadLocal本地變量取值時,會以Thread實例爲Key,獲取其綁定的Value。

一個ThreadLocal實例內部結構的形象展示,大致如圖1-18所示。

圖1-18 一個ThreadLocal(早期版本)實例內部結構的形象展示

Java程序可以使用ThreadLocal的成員方法進行本地值操作,具體的成員方法如表1-2所示。

表1-2 ThreadLocal的成員方法

方 法 說 明
set(T value) 設置當前線程在“線程本地變量”實例中綁定的本地值
T get() 獲得當前線程在“線程本地變量”實例中綁定的本地值
remove() 移除當前線程在“線程本地變量”實例中綁定的本地值

下面的例子,通過ThreadLocal的成員方法進行“線程本地變量”中線程本地值的設置、獲取、移除,具體的代碼如下:

package com.crazymakercircle.mutithread.basic.threadlocal;
...省略import
public class ThreadLocalTest
{
    @Data
    static class Foo
    {
        //實例總數
        static final AtomicInteger AMOUNT = new AtomicInteger(0);
        //對象的編號
        int index = 0;
        //對象的內容
        int bar = 10;
        //構造器
        public Foo()
        {
            index = AMOUNT.incrementAndGet(); //總數增加,並且給對象的編號
        }
        @Override
        public String toString()
        {
            return index + "@Foo{bar=" + bar + '}';
        }
    }

    //定義線程本地變量
    private static final ThreadLocal<Foo> LOCAL_FOO =  new ThreadLocal<Foo>();

    public static void main(String[] args) throws InterruptedException
    {
         //獲取自定義的混合型線程池
        ThreadPoolExecutor threadPool = 
                                ThreadUtil.getMixedTargetThreadPool();

        //提交5個任務,將會用到5個線程
        for (int i = 0; i < 5; i++)
        {
            threadPool.execute(new Runnable()
            {
                @Override
                public void run()
                {
                    //獲取“線程本地變量”中當前線程所綁定的值
                    if (LOCAL_FOO.get() == null)
                    {
                        //設置“線程本地變量”中當前線程所綁定的值
                        LOCAL_FOO.set(new Foo());
                    }
                       Print.tco("初始的本地值:" + LOCAL_FOO.get());
                    //每個線程執行10次
                    for (int i = 0; i < 10; i++)
                    {
                        Foo foo = LOCAL_FOO.get();
                        foo.setBar(foo.getBar() + 1);  //值增1
                        sleepMilliSeconds(10);
                    }
                    Print.tco("累加10次之後的本地值:" + LOCAL_FOO.get());

                    //刪除“線程本地變量”中當前線程所綁定的值
                    LOCAL_FOO.remove(); //這點對於線程池中的線程尤其重要
                }
            });
        }
    }
}

運行以上示例,其結果如下:

[apppool-1-mixed-3]:初始的本地值:3@Foo{bar=10}
[apppool-1-mixed-4]:初始的本地值:4@Foo{bar=10}
[apppool-1-mixed-5]:初始的本地值:5@Foo{bar=10}
[apppool-1-mixed-2]:初始的本地值:1@Foo{bar=10}
[apppool-1-mixed-1]:初始的本地值:2@Foo{bar=10}
[apppool-1-mixed-1]:累加10次之後的本地值:2@Foo{bar=20}
[apppool-1-mixed-3]:累加10次之後的本地值:3@Foo{bar=20}
[apppool-1-mixed-5]:累加10次之後的本地值:5@Foo{bar=20}
[apppool-1-mixed-2]:累加10次之後的本地值:1@Foo{bar=20}
[apppool-1-mixed-4]:累加10次之後的本地值:4@Foo{bar=20}

通過輸出的結果可以看出,在“線程本地變量”(LOCAL_FOO)中,每一個線程都綁定了一個獨立的值(Foo對象),這些值對象是線程的私有財產,可以理解爲線程的本地值

每一次操作都是在自己的同一個本地值上進行,例子中通過線程本地值的index始終一致可以看出,每個線程操作的是同一個Foo對象。
如果線程尚未在本地變量(如LOCAL_FOO)中綁定了一個值,直接通過get( )方法去獲取本地值,會獲取到一個空值,此時可以通過set( )方法設置一個值作爲初始值,具體的代碼如下所示:

    //獲取“線程本地變量”中當前線程所綁定的值
     if (LOCAL_FOO.get() == null)
    {
        //設置“線程本地變量”中當前線程所綁定的初始值
        LOCAL_FOO.set(new Foo());
    }

在當前線程尚未綁定值時,如果希望能從線程本地變量獲取 初始值,而且也不想採用以上的“判空後設值”的繁瑣 方式,則可以在定義使用 ThreadLocal.withInitial(…)靜態工廠方法,

使用ThreadLocal.withInitial(…) 的方式,可以在定義ThreadLocal對象時設置一個獲取初始值的回調函數,具體的代碼如下所示:

ThreadLocal<Foo> LOCAL_FOO = ThreadLocal.withInitial(() -> new Foo());

以上代碼並沒有使用new ThreadLocal()去構造一個ThreadLocal對象,而是使用withInitial(…)工廠方法創建一個ThreadLocal對象,並傳遞了一個獲取初始值的Lamda回調函數。

在線程尚未綁定值而直接從“線程本地變量”獲取值時,將會取得回調函數被調用之後所返回的值。

ThreadLocal的成員方法

Java程序可以使用ThreadLocal的成員方法進行本地值操作,具體的成員方法如下所示:

  • set(T value) :設置當前線程在“線程本地變量”實例中綁定的本地值

  • T get() :獲得當前線程在“線程本地變量”實例中綁定的本地值

  • remove() :移除當前線程在“線程本地變量”實例中綁定的本地值

  • initialValue( ) : 當“線程本地變量”在當前線程的ThreadLocalMap中尚未綁定值時,該方法用於獲取初始值。

ThreadLocal的作用和優劣勢

ThreadLocal的作用和優劣勢主要體現在以下幾個方面:

作用
  1. 線程本地存儲:ThreadLocal爲每個線程提供各自的變量副本,每個線程都可以讀取和修改自己線程的本地變量。這意味着在多線程環境中,不同線程對ThreadLocal變量的操作是獨立的,不會互相干擾。

  2. 簡化線程間數據傳遞:通過使用ThreadLocal,可以將某些需要在多線程之間共享但又需要避免競態條件的數據封裝起來,每個線程訪問的都是自己的數據副本,從而簡化了線程間數據傳遞的複雜性。

  3. 管理線程特定資源:ThreadLocal常用於存儲線程上下文信息,如用戶會話信息、事務信息等,這些信息通常與特定線程關聯,不需要在多個線程之間共享。

優勢
  1. 線程安全:由於每個線程操作的是自己的變量副本,因此避免了多線程訪問共享變量時可能出現的競態條件和數據不一致問題,從而保證了線程安全。

  2. 簡化編程模型:通過使用ThreadLocal,開發者可以更加專注於業務邏輯的實現,而不需要過多關注線程間數據同步和共享的問題,降低了編程複雜度。

  3. 性能優化:由於避免了線程間數據同步的開銷,以及減少了不必要的鎖競爭,因此在某些場景下,使用ThreadLocal可以提高系統的併發性能。

劣勢
  1. 內存消耗:ThreadLocal爲每個線程創建變量副本,這意味着當線程數量較多時,會佔用較多的內存資源。特別是在長時間運行的系統中,如果線程頻繁創建和銷燬,可能會導致內存泄漏問題。

  2. 數據共享限制:由於ThreadLocal變量是線程私有的,因此無法直接實現線程間的數據共享。如果需要在線程間傳遞數據,可能需要藉助其他機制(如消息隊列、共享內存等)。

  3. 使用不當可能導致內存泄露問題:如果開發者在使用ThreadLocal時不小心忘記在線程結束後清理變量(例如通過調用remove()方法),那麼這些變量可能會一直存在於內存中,造成內存泄漏。此外,如果多個線程需要訪問和修改同一份數據,那麼ThreadLocal可能並不適合,因爲它提供的是每個線程私有的變量副本。

  4. 性能開銷:在ThreadLocal中ThreadLocalMap 是一種使用線性探測法實現的哈希表,底層採用數組存儲數據。ThreadLocal.set()/get() 方法在數據密集時很容易出現 Hash 衝突,hash衝突使用的是線性探測法,需要 O(n) 時間複雜度解決衝突問題,效率較低。

綜上所述

ThreadLocal在簡化線程間數據傳遞、管理線程特定資源和提高線程安全性方面具有優勢,但也需要注意其可能帶來的內存消耗和數據共享限制等問題。在使用ThreadLocal時,應根據具體的應用場景和需求進行權衡和選擇。

ThreadLocal的使用場景

ThreadLocal是解決線程安全問題一個較好方案,它通過爲每個線程提供一個獨立的本地值,去解決併發訪問的衝突問題。很多情況下,使用ThreadLocal比直接使用同步機制(如synchronized)解決線程安全問題更簡單,更方便,且結果程序擁有更高的併發性。
ThreadLocal使用場景,大致可以分爲以下兩類:

  1. 線程隔離

    • ThreadLocal的主要價值在於線程隔離,ThreadLocal中數據只屬於當前線程,其本地值對別的線程是不可見的,在多線程環境下,可以防止自己的變量被其他線程篡改。另外,由於各個線程之間的數據相互隔離,避免同步加鎖帶來的性能損失,大大提升了併發性的性能。
    • ThreadLocal在線程隔離的最常用案例爲:可以每個線程綁定一個用戶會話信息、數據庫連接、HTTP請求等,這樣一個線程的所有調用到的處理函數都可以非常方便地訪問這些資源。
    • 常見的ThreadLocal使用場景爲數據庫連接獨享、Session數據管理等場景在“線程隔離”場景中使用ThreadLocal的典型案例爲:可以每個線程綁定一個數據庫連接,是的這個數據庫連接爲線程所獨享,從而避免數據庫連接被混用而導致操作異常問題。
  2. 跨函數傳遞數據

    • 通常用於同一個線程內,跨類、跨方法傳遞數據時,如果不用ThreadLocal,那麼相互之間的數據傳遞勢必要靠返回值和參數,這樣無形之中增加了這些類或者方法之間的耦合度。、

場景1:使用ThreadLocal進行線程隔離

ThreadLocal在“線程隔離”應用場景的典型應用爲“數據庫連接獨享”。下面的代碼來自Hibernate,代碼中通過ThreadLocal進行數據庫連接(Session)的“線程本地化”存儲,主要的代碼如下:

private static final ThreadLocal threadSession = new ThreadLocal();  

public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  
}  

Hibernate對數據庫連接進行了封裝,一個Session 代表一個數據庫連接。通過以上代碼可以看到,

在Hibernate的getSession()方法中,首先判斷當前線程中有沒有放進去session,

如果還沒有,那麼通過sessionFactory().openSession()來創建一個Session ,再將Session 設置到ThreadLocal變量中,這個Session 相當於線程的私有變量,而不是所有線程共用的,顯然其他線程中是取不到這個Session。

一般來說,完成數據庫操作之後程序會將Session關閉,從而節省數據庫連接資源。如果Session的使用方式爲共享而不是獨佔,在這種情況下,Session是多線程共享使用的,如果某個線程使用完成之後,直接將Session關閉,其他線程在操作Session就會報錯。所以Hibernate通過ThreadLocal非常簡單實現了數據庫連接的安全使用。

場景2:使用ThreadLocal進行跨函數數據傳遞

ThreadLocal在“跨函數數據傳遞”應用場景的典型有很多:

  • 用來傳遞請求過程中的用戶ID。

  • 用來傳遞請求過程中的用戶會話(Session)。

  • 用來傳遞HTTP的用戶請求實例HttpRequest。

  • 其他需要在函數之間頻繁傳遞的數據。

以下代碼來自於瘋狂創客圈社羣的微服務腳手架Crazy-SpringCloud工程,通過ThreadLocal在函數之間傳遞用戶信息、會話信息等,並且封裝成了一個獨立的SessionHolder類,具體的代碼如下:

package com.crazymaker.springcloud.common.context;
...省略import
public class SessionHolder
{
    // session id  線程本地變量
    private static final ThreadLocal<String> sidLocal =
                                          new ThreadLocal<>("sidLocal");

    // 用戶信息  線程本地變量
    private static final ThreadLocal<UserDTO> sessionUserLocal =
                                           new ThreadLocal<>("sessionUserLocal");

    // session  線程本地變量
    private static final ThreadLocal<HttpSession> sessionLocal = 
                                              new ThreadLocal<>("sessionLocal");
...省略其他  

    /**
     *保存session在線程本地變量中
      */
    public static void setSession(HttpSession session)
    {
        sessionLocal.set(session);
    }

    /**
     * 取得綁定在線程本地變量中的session 
      */
    public static HttpSession getSession()
    {
        HttpSession session = sessionLocal.get();
        Assert.notNull(session, "session 未設置");
        return session;
    }
    ...省略其他  
}

場景3:ThreadLocal在Java框架中的應用

  1. Spring
    • 在Spring框架中,ThreadLocal用於存儲數據庫連接等線程特定的資源。由於數據庫連接是線程不安全的,因此每個線程都需要有自己的連接副本。Spring通過ThreadLocal將數據庫連接與當前線程關聯起來,從而避免了多線程環境下的數據競爭和不一致問題。
    • 在Spring的事務管理中,ThreadLocal也扮演着重要角色。它確保了每個線程都有自己的事務上下文,包括事務狀態、回滾點等信息,從而實現了事務的隔離性。
  2. MyBatis
    • MyBatis是一個優秀的持久層框架,它支持定製化SQL、存儲過程以及高級映射。在MyBatis中,ThreadLocal可以用來存儲SqlSession對象。由於SqlSession不是線程安全的,因此每個線程都應該擁有自己獨立的SqlSession實例。通過ThreadLocal,MyBatis可以方便地實現SqlSession的線程局部存儲,確保每個線程都能正確地執行SQL操作。
  3. 分佈式系統
    • 在分佈式系統中,ThreadLocal可以用來傳遞全局ID和分支ID等關鍵信息。這些ID對於分佈式事務的追蹤和診斷至關重要。通過將這些ID存儲在ThreadLocal中,可以確保它們在整個請求處理過程中都能被正確傳遞和使用。
  4. 日誌框架
    • 一些日誌框架也利用ThreadLocal來存儲與當前線程相關的日誌上下文信息,如用戶ID、操作類型等。這樣,在記錄日誌時,可以方便地獲取這些信息,並將其添加到日誌條目中,從而方便後續的日誌分析和排查問題。
  5. RPC
    • 在遠程過程調用(RPC)框架中,ThreadLocal用於存儲和傳遞與當前調用相關的上下文信息。這些上下文信息可能包括調用者的身份、調用的參數、超時設置等。通過將這些信息存儲在ThreadLocal中,可以確保它們在RPC調用過程中能夠被正確地傳遞和使用。
  6. Hibernate
    • SessionContext: 用於存儲當前線程的Hibernate會話相關數據,如當前會話、持久化上下文等。
    • TransactionManager: 管理事務狀態,每個線程可以有獨立的事務狀態,如當前是否在事務中。
  7. Tomcat
    • ThreadLocal變量: 用於跟蹤每個請求的會話信息、用戶認證數據等,確保這些數據不會在請求之間共享。
  8. Kafka
    • Producer and Consumer Threads: 在消息生產和消費過程中,使用ThreadLocal來存儲線程特定的配置和狀態信息。

ThreadLocal綜合使用案例

由於ThreadLocal使用不當會導致嚴重的內存泄漏,所以爲了更好的避免內存泄漏的發生,我們使用ThreadLocal時遵守以下兩個原則:

  1. 儘量使用private static final修飾ThreadLocal實例。使用 private 與final 修飾符,主要是儘可能不讓他人修改、變更ThreadLocal變量的引用; 使用static 修飾符主要爲了確保ThreadLocal實例的全局唯一。

  2. ThreadLocal使用完成之後務必調用remove方法。這是簡單、有效地避免ThreadLocal引發內存泄漏的方法。

    下面用一個綜合案例演示一下ThreadLocal的使用。此案例的功能爲:記錄執行過程中所調用的函數的執行耗時。比如在實際Web開發過程中,一次客戶端請求往往會涉及到DB、緩存、RPC等多個耗時調用,一旦出現性能問題,就需要記錄一下各個點耗時的時間,從而判斷性能的瓶頸所在。
    下面的代碼定義了三個方法 serviceMethoddaoMethodrpcMethod,用於模擬實際的DB、RPC等耗時調用,具體的代碼如下:

package com.crazymakercircle.mutithread.basic.threadlocal;
...省略import
public class ThreadLocalTest2
{
    /**
     * 模擬業務方法
     */
    public void serviceMethod()
    {
        //睡眠500ms,模擬執行耗時
        sleepMilliSeconds(500);

        //記錄從開始調用到當前這個點( "point-1")的耗時
        SpeedLog.logPoint("point-1 service");

        //調用DAO方法:模擬dao業務方法
        daoMethod();

        //調用RPC方法:模擬RPC遠程業務方法
        rpcMethod();
    }

    /**
     * 模擬dao業務方法
     */
    public void daoMethod()
    {
        //睡眠400ms,模擬執行耗時
        sleepMilliSeconds(400);

        //記錄上一個點("point-1")這裏("point-2")的耗時
        SpeedLog.logPoint("point-2 dao");
    }

    /**
     * 模擬RPC遠程業務方法
     */
    public void rpcMethod()
    {
        //睡眠400ms,模擬執行耗時
        sleepMilliSeconds(600);

        //記錄上一個點("point-2")這裏("point-3")的耗時
        SpeedLog.logPoint("point-3 rpc");
    }
    ...省略不相干代碼
}

爲了能靈活地記錄各個執行埋點的耗時,這裏定義了一個SpeedLog類。該類含有一個ThreadLocal類型的、初始值爲一個Map<String, Long>實例的“線程本地變量”,名字叫做TIME_RECORD_LOCAL。
如果要記錄某個函數的調用耗時,就需要進行耗時埋點,具體的方法爲logPoint(String point)。該方法會操作TIME_RECORD_LOCAL本地變量,在其中增加一次耗時記錄:Key爲耗時埋點的名稱,值爲當前時間和上一次記錄時間的差值,也就是上一次埋點到本次埋點之間的調用耗時。
SpeedLog類的代碼,大致如下:

package com.crazymakercircle.mutithread.basic.threadlocal;
...省略import
public class SpeedLog
{
    /**
     * 記錄調用耗時的本地Map變量
     */
private static final ThreadLocal<Map<String, Long>>
 TIME_RECORD_LOCAL =ThreadLocal.withInitial(SpeedLog::initialStartTime);

    /**
     * 記錄調用耗時的本地Map變量的初始化方法
     */
    public static Map<String, Long> initialStartTime()
    {
        Map<String, Long> map = new HashMap<>();
        map.put("start", System.currentTimeMillis());
        map.put("last", System.currentTimeMillis());
        return map;
    }

    /**
     * 開始耗時記錄
     */
    public static final void beginSpeedLog()
    {
        Print.fo("開始耗時記錄");
        TIME_RECORD_LOCAL.get();
    }

    /**
     * 結束耗時記錄
     */
    public static final void endSpeedLog()
    {
        TIME_RECORD_LOCAL.remove();
        Print.fo("結束耗時記錄");
    }

    /**
     * 耗時埋點
     */
    public static final void logPoint(String point)
    {
        //獲取上一次的時間
        Long last = TIME_RECORD_LOCAL.get().get("last");
        //計算上一次埋點到當前埋點的耗時
        Long cost = System.currentTimeMillis() - last;

        //保存上一次埋點到當前埋點的耗時
        TIME_RECORD_LOCAL.get().put(point + " cost:", cost);

         //保存當前時間,供下一次埋點使用
        TIME_RECORD_LOCAL.get().put("last", System.currentTimeMillis());
    }
    ...省略不相干代碼
}

下面是一個測試用例,演示一下在 serviceMethod、daoMethod、rpcMethod三個模擬方法的調用過程中,其耗時的記錄和輸出,具體的代碼如下:

package com.crazymakercircle.mutithread.basic.threadlocal;
...省略import
public class ThreadLocalTest2
{

    /**
     * 測試用例:線程方法調用的耗時
     */
    @org.junit.Test
    public void testSpeedLog() throws InterruptedException
    {
        Runnable runnable = () ->
        {
            //開始耗時記錄,保存當前時間
            SpeedLog.beginSpeedLog();
            //調用模擬業務方法
            serviceMethod();
            //打印耗時
             SpeedLog.printCost();
            //結束耗時記錄
            SpeedLog.endSpeedLog();

        };
        new Thread(runnable).start();
        sleepSeconds(10);//等待10s看結果
    }
    ...省略不相干代碼
}

運行以上用例,三個模擬方法 serviceMethod、daoMethod、rpcMethod的耗時輸出如下:

[SpeedLog.beginSpeedLog]:開始耗時記錄
[SpeedLog.printCost]:start =>1600347227334
[SpeedLog.printCost]:point-1 service cost: =>500
[SpeedLog.printCost]:point-2 dao cost: =>401
[SpeedLog.printCost]:point-3 rpc cost: =>600
[SpeedLog.printCost]:last =>1600347228835
[SpeedLog.endSpeedLog]:結束耗時記錄

以上案例中,將ThreadLocal變量聲明成爲private static final的形式,使得外部不能直接訪問,外部能訪問的是將ThreadLocal變量封裝之後的接口函數如beginSpeedLog( )、logPoint(String point)、endSpeedLog( )等等。
總之,使用ThreadLocal能實現每個線程都有一份變量的本地值,其原因是由於每個線程都有自己獨立的ThreadLocalMap空間,本質上屬於以空間換時間的設計思路,該設計思路屬於了另一種意義的 “無鎖編程”。

ThreadLocal 使用總結

總結起來,ThreadLocal在實際項目中的應用廣泛且實用,但同時也需注意其潛在的風險和挑戰。通過遵循最佳實踐,合理設計和管理ThreadLocal變量,我們能夠在多線程環境下高效地解決數據隔離問題,同時保持代碼簡潔易懂,確保系統穩定性和高性能運行。

ThreadLocal的實現原理

在早期的JDK版本中,ThreadLocal的內部結構是一個Map,其中每一個線程實例作爲Key,線程在“線程本地變量”中綁定的值爲Value(本地值)。早期版本中的Map結構,其擁有者爲ThreadLocal,每一個ThreadLocal實例,擁有一個Map實例。

在JDK8版本中,ThreadLocal的內部結構發生了演進,雖然還是使用了Map結構,但是Map結構的擁有者已經發生了變化,其擁有者爲Thread(線程)實例,每一個Thread實例,擁有一個Map實例。另外,Map結構的Key值也發生了變化:新的Key爲ThreadLocal實例。

在JDK8版本中,每一個Thread線程內部都有一個Map(ThreadLocalMap),因爲如我們給一個Thread創建多個Threadlocal實列,然乎放置本地數據,那麼當前線程的ThreadLocalMap中就會有多個“Key-Value對”,其中ThreadLocal實例爲key,本地數據爲Value。

在代碼的層面來說,新版本的ThreadLocalMap還是由ThreadLocal類維護的,由ThreadLocal負責ThreadLocalMap實例的獲取、創建,並從中設置本地值、獲取本地值。所以ThreadLocalMap還寄存於ThreadLocal內部,而並沒有被遷移到Thread內部。

ThreadLocal內部使用ThreadLocalMap來保存每個線程的變量副本。每個Thread都持有一個ThreadLocalMap的引用,這個map的key是ThreadLocal實例本身,value是線程變量副本。

當線程首次調用ThreadLocal的get()或set()方法時,ThreadLocalMap會被創建並關聯到當前線程。此後,線程就可以通過ThreadLocal實例的get()和set()方法訪問自己的線程局部變量了。

簡單來說:ThreadLocal 就是一種以空間換時間的做法,在每個 Thread 裏面維護了一個以開放定址法實現的ThreadLocal.ThreadLocalMap,把數據進行隔離,數據不共享,自然就沒有線程安全方面的問題了。

ThreadLocal內部結構演進

在早期的JDK版本中,ThreadLocal的內部結構是一個Map,其中每一個線程實例作爲Key,線程在“線程本地變量”中綁定的值爲Value(本地值)。早期版本中的Map結構,其擁有者爲ThreadLocal,每一個ThreadLocal實例,擁有一個Map實例。

​ 在JDK8版本中,ThreadLocal的內部結構發生了演進,雖然還是使用了Map結構,但是Map結構的擁有者已經發生了變化,其擁有者爲Thread(線程)實例,每一個Thread實例,擁有一個Map實例。另外,Map結構的Key值也發生了變化:新的Key爲ThreadLocal實例

在JDK8版本中,每一個Thread線程內部都有一個Map(ThreadLocalMap),因爲如我們給一個Thread創建多個Threadlocal實列,然乎放置本地數據,那麼當前線程的ThreadLocalMap中就會有多個“Key-Value對”,其中ThreadLocal實例爲key本地數據爲Value

在代碼的層面來說,新版本的ThreadLocalMap還是由ThreadLocal類維護的,由ThreadLocal負責ThreadLocalMap實例的獲取、創建,並從中設置本地值、獲取本地值。

所以ThreadLocalMap還寄存於ThreadLocal內部,而並沒有被遷移到Thread內部。

每一個線程在獲取本地值時,將ThreadLocal實例作爲Key從自己擁有的ThreadLocalMap中獲取值,別的線程無法訪問自己的ThreadLocalMap實例,自己也無法訪問別人ThreadLocalMap實例,達到相互隔離,互不干擾。

jdk1.8版本ThreadLocalMap 與早期版本的ThreadLocalMap實現相比,主要的變化爲:

  • 擁有者發生了變化:新版本的ThreadLocalMap擁有者爲Thread,早期版本的ThreadLocalMap擁有者爲ThreadLocal

  • Key發生了變化:新版本的Key爲ThreadLocal實例,早期版本的Key爲Thread實例。

jdk1.8版本ThreadLocalMap 與早期版本的ThreadLocalMap實現相比,新版本的主要優勢爲

(1)每個ThreadLocalMap存儲的“Key-Value對”數量變少

  • 早期版本的“Key-Value對”數量與線程個數強關聯,如果線程數量多,則ThreadLocalMap存儲“Key-Value對”數量也多。

  • 新版本的ThreadLocalMap的Key 爲ThreadLocal實例,多線程情況下ThreadLocal實例比線程數少。

(2)ThreadLocalMap擁有者爲Thread,降低內存消耗

  • 早期版本ThreadLocalMap的擁有者爲ThreadLocal,在Thread(線程)實例銷燬後,ThreadLocalMap還是存在的;
  • 新版本的ThreadLocalMap的擁有者爲Thread,現在當Thread實例銷燬後,ThreadLocalMap也會隨之銷燬,在一定程度上能減少內存的消耗。

ThreadLocalMap對象和Entry是什麼

ThreadLocal的操作都是基於ThreadLocalMap展開的,而ThreadLocalMap是ThreadLocal的一個靜態內部類,其實現了一套簡單的Map結構(比HashMap簡單)。

ThreadLocalMap內部維護Entry數組,作爲ThreadLocalMap條目數組,作爲散列表使用,如下圖:

ThreadLocal的結構模型

以jdk1.8的ThreadLocal 爲標準, 總結一下 ThreadLocal的結構模型。

每個Thread都有一個 ThreadLocalMap 結構,其中就保存着當前線程所持有的所有 ThreadLocal。

ThreadLocal 本身只是一個引用,沒有直接保存值,值是保存在 ThreadLocalMap 中,ThreadLocal 作爲 key,值(實際保存的數據)作爲 value。

可以用下面的圖來概括:

ThreadLocal源碼分析

ThreadLocal的源碼提供的方法不多,主要的方法有:set(T value)方法get( )方法remove( )方法initialValue( )方法

set(T value)方法

set(T value)方法用於設置“線程本地變量”在當前線程的ThreadLocalMap中對應的值,相當於設置線程本地值,其核心源碼如下:

    public void set(T value) {
        //獲取當前線程對象
        Thread t = Thread.currentThread(); 

        //獲取當前線程的ThreadLocalMap 成員
        ThreadLocalMap map = getMap(t);

        //判斷map是否存在
        if (map != null) 
        { 
                //value被綁定到threadLocal實例
            map.set(this, value);   
        }
        else
        {
            // 如果當前線程沒有ThreadLocalMap成員實例
            // 創建一個ThreadLocalMap實例,然後,作爲成員關聯到t(thread實例)
            createMap(t, value);
        }
    }

    // 獲取線程t的ThreadLocalMap成員
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    // 線程t創建一個ThreadLocalMap成員
    //併爲新的Map成員設置第一個Key-Value對,Key爲當前的ThreadLocal實例
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

通過以上的源碼,可以看出set(T value)方法的執行流程,大致如下:

  • 獲得當前線程,然後獲得當前線程的ThreadLocalMap成員,暫存於map變量。
  • 如果map不爲空,則將Value設置到map中,當前的Threadlocal作爲key。
  • 如果map爲空,給該線程創建map,然後設置第一個“Key-Value對”,Key爲當前的ThreadLocal實例,Value爲set方法的參數value值。

get( )方法

get( )方法用於獲取“線程本地變量”在當前線程的ThreadLocalMap中對應的值,相當於獲取線程本地值,其核心源碼如下:

public T get() {
    //獲得當前線程對象
    Thread t = Thread.currentThread();
    //獲得線程對象的ThreadLocalMap 內部成員
    ThreadLocalMap map = getMap(t);

   // 如果當前線程的內部map成員存在
   if (map != null) {
        // 以當前threadlocal爲Key,嘗試獲得條目
        ThreadLocalMap.Entry e = map.getEntry(this);
       // 條目存在
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    // 如果當前線程對應map不存在
    //或者map存在,但是當前threadlocal實例沒有對應的Key-Value,返回初始值
    return setInitialValue();
}

// 設置threadlocal關聯的初始值並返回
private T setInitialValue() {
    //調用初始化鉤子函數,獲取初始值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

通過以上的源碼,可以看出T get()方法的執行流程,大致如下:

  • 先嚐試獲得當前線程,然後獲得當前線程的ThreadLocalMap成員,暫存於map變量。
  • 如果獲得的map不爲空,以當前threadlocal實例爲Key嘗試獲得map中的Entry(條目)。
  • 如果Entry條目不爲空,返回Entry中的Value。
  • 如果Entry爲空,則通過調用initialValue初始化鉤子函數獲取“ThreadLocal”初始值,並設置在map中。如果map不存在,還會給當前線程創建新ThreadLocalMap成員,並綁定第一個“Key-Value對”。

remove( )方法

remove()方法用於在當前線程的ThreadLocalMap中,移除“線程本地變量”所對應的值,其核心源碼如下:

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

initialValue( ) 方法

當“線程本地變量”在當前線程的ThreadLocalMap中尚未綁定值時,initialValue( )方法用於獲取初始值。其源碼如下:

   protected T initialValue() {
        return null;
   }

如果沒有調用set直接調用get,則會調用此方法,但是該方法只會被調用一次。

默認情況下,initialValue( )方法返回null,如果不想返回null,可以繼承ThreadLocal覆蓋此方法。

真的需要繼承ThreadLocal去重寫initialValue()方法嗎?其實沒有必要。

JDK已經爲大家定義了一個ThreadLocal的內部SuppliedThreadLocal靜態子類,並且提供了 ThreadLocal.withInitial(…)靜態工廠方法,方便大家在定義ThreadLocal實例時設置初始值回調函數。

使用工廠方法構造ThreadLocal實例的代碼如下:

ThreadLocal<Foo> LOCAL_FOO = ThreadLocal.withInitial(() -> new Foo());

JDK定義的ThreadLocal.withInitial(…)靜態工廠方法,以及其內部子類SuppliedThreadLocal的源碼如下:

    //ThreadLocal工廠方法,可以設置本地變量初始值鉤子函數
    public static <S> ThreadLocal<S> withInitial(
                                Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    //內部靜態子類
    //繼承了ThreadLocal,重寫了initialValue()方法,返回鉤子函數的值作爲初始值
    static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
        //保存鉤子函數
        private final Supplier<? extends T> supplier;
        //傳入鉤子函數
        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }

        @Override
        protected T initialValue() {
            return supplier.get();  //返回鉤子函數的值作爲初始值
        }
    }

ThreadLocalMap源碼分析

ThreadLocal的操作都是基於ThreadLocalMap展開的,而ThreadLocalMap是ThreadLocal的一個靜態內部類,其實現了一套簡單的Map結構(比HashMap簡單)。

ThreadLocalMap的主要成員變量

ThreadLocalMap的成員變量與HashMap的成員變量非常類似,其內部的主要成員如下所示:

public class ThreadLocal<T> {
    ...省略其他                            
static class ThreadLocalMap {  
          //Map的條目數組,作爲散列表使用
        private Entry[] table;
          //Map的條目初始容量16
         private static final int INITIAL_CAPACITY = 16;
        //Map的條目數量 
        private int size = 0;
       //擴容因子
        private int threshold; 

        //Map的條目類型,一個靜態的內部類
       // Entry 繼承子WeakReference,Key爲ThreadLocal實例
       static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value; //條目的值
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    ...省略其他        
} 

ThreadLocal源碼中get()set( )remove()方法都涉及到ThreadLocalMap的方法調用,主要調用了ThreadLocalMap的如下幾個函數:

  • set(ThreadLocal<?> key, Object value) :向Map實例設置“Key-Value對”。
  • getEntry(ThreadLocal):從Map實例獲取Key(ThreadLocal實例)所屬的Entry。
  • remove(ThreadLocal):根據Key(ThreadLocal實例)從Map實例移除所屬的Entry。

作爲參考,這裏只將ThreadLocalMapset(ThreadLocal<?> key, Object value) 方法的代碼以註釋的形式做一個簡單的分析,具體如下:

        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;

             //根據key的HashCode,找到key在數組上的槽點i
             int i = key.threadLocalHashCode & (len-1);

            // 從槽點i開始向後循環搜索,找空餘槽點(空餘位置)或者找現有槽點
            //如果沒有現有槽點,則必定有空餘槽點,因爲沒有空間時會擴容 
            for (Entry e = tab[i];   e != null; 
                                     e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //找到現有槽點:Key值爲ThreadLocal實例
                if (k == key) {
                    e.value = value;
                    return;
                }
                //找到異常槽點:槽點被GC掉,重設Key值和Value值 
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //沒有找到現有的槽點,增加新的Entry
            tab[i] = new Entry(key, value);
            //設置ThreadLocal數量
            int sz = ++size;

            //清理Key爲null的無效Entry
            //沒有可清理的Entry,並且現有條目數量大於擴容因子值,進行擴容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

Entry的Key需要使用弱引用

Entry用於保存ThreadLocalMap的“Key-Value”條目,但是Entry使用了對Threadlocal實例進行包裝之後的弱引用(WeakReference)作爲Key,其代碼如下:

// Entry 繼承了WeakReference,並使用WeakReference對Key進行包裝
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value; //值
    Entry(ThreadLocal<?> k, Object v) {
        super(k);  //使用WeakReference對Key值進行包裝
        value = v;
    }
}

爲什麼Entry需要使用弱引用對Key進行包裝,而不是直接使用Threadlocal實例作爲Key呢?

這個問題有點兒複雜,如果要分析清楚還有點難度。這裏從一個簡單的例子入手,假設有一個方法funcA( )創建了一個“線程本地變量”,具體如下:

    public void funcA()
    {
        //創建一個線程本地變量
        ThreadLocal local = new ThreadLocal<Integer>(); 
        //設置值
        local.set(100);   
        //獲取值
        local.get();  
        //函數末尾
    }

當線程tn執行funcA方法到其末尾時,線程tn相關的JVM棧內存以及內部ThreadLocalMap成員的結構,大致如圖1-20所示。

線程tn調用funcA()方法新建了一個ThreadLocal實例,並使用local 局部變量指向這個實例,並且此local 是強引用;

在調用local.set(100)之後,線程tn的ThreadLocalMap成員內部會新建一個Entry實例,其Key 以弱引用包裝的方式指向ThreadLocal實例。

當線程tn執行完funcA方法後,funcA的方法棧幀將被銷燬,強引用 local 的值也就沒有了,但此時線程的ThreadLocalMap裏的對應的Entry的 Key 引用還指向了ThreadLocal實例。

若Entry的 Key 引用是強引用就會導致Key引用指向的ThreadLocal實例、及其Value值都不能被GC回收,這將造成嚴重的內存泄露,具體如圖1-21所示。

圖1-20 當線程tn執行funcA方法末尾時內存結構

圖1-21 若Entry的Key爲強引用將導致ThreadLocal實例不能回收

什麼是弱引用呢?

僅有弱引用(WeakReference)指向的對象,只能生存到下一次垃圾回收之前。

換句話說,當GC發生時,不管內存夠不夠,僅有弱引用所指向的對象都會被回收。而擁有強引用指向的對象,則不會被直接回收。

什麼是內存泄漏呢?

不再用到的內存,沒有及時釋放,就叫做內存泄漏。

對於持續運行的服務進程,必須及時釋放內存,否則內存佔用率越來越高,輕則影響系統性能,重則導致進程崩潰。

由於ThreadLocalMap中Entry的 Key 使用了弱引用,在下次GC發生時,就可以使那些沒有被其他強引用指向、僅被Entry的Key 所指向的ThreadLocal實例能被順利回收。並且,在Entry的Key引用被回收之後,其Entry的Key值變爲null。後續當ThreadLocal的getsetremove被調用時,ThreadLocalMap的內部代碼會清除這些Key爲null的Entry,從而完成相應的內存釋放。

總結一下,使用ThreadLocal會發生內存泄漏的前提條件:

  • 線程長時間運行而沒有被銷燬。線程池中的Thread實例很容易滿足此條件。
  • ThreadLocal引用被設置爲null,且後續在同一Thread實例的執行期間,沒有發生對其他ThreadLocal實例的get、set或remove操作。

只要存在一個針對任何ThreadLocal實例的get、set或remove操作,就會觸發Thread實例擁有的ThreadLocalMap的Key爲null的Entry清理工作,釋放掉ThreadLocal弱引用爲null的Entry。

綜合以上兩條可以看出:使用ThreadLocal出現內存泄漏還是比較容易的。但是一般公司對如何使用ThreadLocal都有編程規範要求,只要大家按照規範編寫程序,也沒有那麼容易發生內存泄漏。

ThreadLocal造成內存泄露的問題

什麼是內存泄漏?

不再用到的內存,沒有及時釋放,就叫做內存泄漏。

對於持續運行的服務進程,必須及時釋放內存,否則內存佔用率越來越高,輕則影響系統性能,重則導致進程崩潰。

ThreadLocal是怎麼造成內存泄露的呢?

如果發生了下面的情況:

  • 如果ThreadLocal是null了,也就是要被GC回收了,

  • 但是此時我們的ThreadLocalMap(thread 的內部屬性)生命週期和Thread的一樣,它不會回收,這時候就出現了一個現象。

總之,就是ThreadLocalMap的key沒了,但是value還在,這就造成了內存泄漏。

我們細緻的分析一下。

ThreadLocal 有兩個引用鏈

ThreadLocalMap中的Key就是ThreadLocal對象,ThreadLocal 有兩個引用鏈:

  • 一個引用鏈是棧內存中ThreadLocal引用
  • 一個引用鏈是ThreadLocalMap中的Key對它的引用

而對於Value(實際保存的值)來說,它的引用鏈只有一條,就是從Thread對象引用過來的,如下圖:

上述過程分析後,就會出現如下的兩種情況:

情況1: key的泄漏

情況2: value的泄漏

情況1:key的泄漏

棧上的ThreadLocal Ref引用不再使用了,即當前方法結束處理後,這個對象引用就不再使用了,

那麼,ThreadLocal對象因爲還有一條引用鏈存在,如果是強引用的話,這裏就會導致ThreadLocal對象無法被回收,可能導致OOM。

情況1 的解決方案,使用弱引用解決 。

情況2: value的泄漏

情況2.假設我們使用了線程池,如果Thread對象一直被佔用使用中(如在線程池中被重複使用),但是此時我們的ThreadLocalMap(thread 的內部屬性)生命週期和Thread的一樣,它不會回收,這時候就出現了一個現象。

這就意味着,Value這條引用鏈就一直存在,那麼就會導致ThreadLocalMap無法被JVM回收,可能導致OOM,如上圖。

情況2 ,比較嚴重。還得另想辦法。

情況1的解決方案:使用弱引用,解決key的內存泄露

從如下ThreadLocal中內部類Entry代碼可知:

Entry類的父類是弱引用WeakReference,ThreadLocal的引用k通過 WeakReference 構造方法傳遞給了 父類WeakReference的構造方法,

從而,ThreadLocalMap中的Key是ThreadLocal的弱引用,通過弱引用來解決內存泄露問題。

具體的代碼如下

static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k); // key爲弱引用
                value = v;
            }
        }
}

棧內存中的ThreadLocal Ref引用不再使用了,即噹噹前方法結束處理後,這個key對象引用就不再使用了,

那麼,如果這裏 不用弱引用而是強引用的話,這裏ThreadLocal對象因爲還有一條引用鏈存在,所以就會導致他無法被回收,可能導致OOM。

回顧Java中4種引用類型

  1. 強引用(Strong Reference)
    • 這是最常見的引用類型。一個對象具有強引用,垃圾收集器就不會回收它,即使系統內存空間不足。
    • 示例:Object obj = new Object(); 在這裏,obj就是new Object()的一個強引用。
  2. 軟引用(Soft Reference)
    • 用來描述一些可能還有用但並非必需的對象。在系統將要發生內存溢出異常前,將會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。
    • 在Java中,軟引用是用來實現內存敏感的高速緩存。
    • 示例:使用java.lang.ref.SoftReference類可以創建軟引用。
  3. 弱引用(Weak Reference)
    • 這裏討論ThreadLocalMap中Entry類的重點。
    • 弱引用也是用來描述非必需對象的,它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收被弱引用關聯的對象。
    • 在Java中,弱引用是用來描述那些非關鍵的數據,在Java裏用java.lang.ref.WeakReference類來表示。
    • 示例:使用java.lang.ref.WeakReference類可以創建弱引用。
  4. 虛引用(Phantom Reference)
    • 一個虛引用關聯着的對象,在任何時候都可能被垃圾收集器回收,它不能單獨用來獲取被引用的對象。虛引用必須和引用隊列(ReferenceQueue)聯合使用。主要用來跟蹤對象被垃圾回收的活動。
    • 虛引用對於一般的應用程序來說意義不大,主要使用在能比較精確控制Java垃圾收集器的高級場景中。
    • 示例:使用java.lang.ref.PhantomReference類可以創建虛引用。

弱引用也是用來描述非必需對象的,它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收被弱引用關聯的對象。

從而解決 key的泄漏問題。

情況2的解決方案:清理策略解決value內存泄露

爲了解決value內存泄露問題,Java 的 ThreadLocal 實現了兩大清理方式:

  • 探測式清理(Proactive Cleanup)
  • 啓發式清理(Heuristic Cleanup) 。

源碼:value的 探測式清理 :

當線程調用 ThreadLocalget()set()remove()方法時,會觸發對 ThreadLocalMap 的清理。

此時,ThreadLocalMap 會檢查所有鍵(ThreadLocal 實例),並移除那些已經被垃圾回收的key鍵及其對應的value 值。

這種清理是主動的,因爲它是在每次操作 ThreadLocal 時進行的。

探測式清理(Proactive Cleanup)如何實現的呢?

從當前節點開始遍歷數組,將key等於null的entry置爲null,key不等於null則rehash重新分配位置,若重新分配上的位置有元素則往後順延。

注意:這裏把清理的開銷放到了get、set操作上,如果get的時候無用Entry(Entry的Key爲null)特別多,那這次get相對而言就比較慢了。

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    // 將k=null的entry置爲null
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    // k不爲null,則rehash從新分配配置
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            // 重新分配後的位置上有元素則往後順延。
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

源碼:value的啓發式清理:

ThreadLocalMap 的 set() 方法中,有一個閾值(默認爲 ThreadLocalMap.Entry 數組長度的 1/4)。

當 ThreadLocalMap 中的 Entry 對象被刪除(通過鍵的弱引用被垃圾回收)並且剩餘的 Entry 數量大於這個閾值時,會觸發一次啓發式清理操作。

這種清理是啓發式的,因爲它不是每次操作都進行,而是基於一定的條件和概率。

啓發式清理(Heuristic Cleanup)如何實現?

從當前節點開始,進行do-while循環檢查清理過期key,結束條件是連續n次未發現過期key就跳出循環,n是經過位運算計算得出的,可以簡單理解爲數組長度的2的多少次冪次。

private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    // 移除
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

業務主動清理:手動清除解決內存泄露

儘管有弱引用以及這些清理機制,但最佳實踐業務主動清理,

如何業務主動清理?在使用完 ThreadLocal 後顯式調用 remove()方法,以確保不再需要的值能夠被及時回收,key和value 都同時清理,一鍋端。

這樣可以避免潛在的內存泄漏問題,並減少垃圾回收的壓力。

ThreadLocal與內存泄露:防範與診斷

ThreadLocal的一個常見問題是內存泄露。

這通常發生在使用線程池的場景中,因爲線程池中的線程通常是長期存在的,它們的ThreadLocal變量也不會自動清理,這可能導致內存泄漏。

JDK 用了三個辦法,來解決內存泄漏。

業務上解決這個問題的一個方法是,每當使用完ThreadLocal變量後,顯式地調用remove()方法來清除它:

使用ThreadLocal的性能問題和優化措施

雖然ThreadLocal提供了很方便的線程隔離機制,但有性能損耗的。

ThreadLocal的性能開銷

ThreadLocal的性能開銷主要來自兩個方面:

  • ThreadLocalMap的維護。
  • ThreadLocal變量的創建和銷燬。

在使用ThreadLocal時,尤其是在高併發的環境下,要注意其對性能的影響。

因此,在使用ThreadLocal時,要儘量複用、重用ThreadLocal變量,避免在高頻率的操作中頻繁地創建和銷燬它們。`

編程規範:推薦使用 static final 修飾ThreadLocal對象

如何 要儘量複用、重用ThreadLocal變量?

編程規範有云:ThreadLocal 實例作爲ThreadLocalMap的Key,針對一個線程內所有操作是共享的,所以建議設置static修飾符,以便被所有的對象共享。

由於靜態變量會在類第一次被使用時裝載,只會分配一次存儲空間,此類的所有實例都會共享這個存儲空間,所以使用 static修飾ThreadLocal 就會節約內存空間。

另外,爲了確保ThreadLocal 實例的唯一性,除了使用static修飾之外,還會使用final進行加強修飾,以防止其在使用過程中發生動態變更。參考的實例如下:

 //推薦使用static final線程本地變量
 private static final ThreadLocal<Foo> LOCAL_FOO = new ThreadLocal<Foo>();

以上代碼,爲什麼ThreadLocal實例除了添加static final 修飾之後,還常常加上了 private修飾呢?

主要目的是 縮小使用的範圍,儘可能不讓他人引用。

凡事都有兩面性,使用static 、final修飾ThreadLocal實例也會帶來副作用: 內存泄漏。

  • 爲啥內存泄露又出來了?

  • 上面不是解決了嗎?

  • 嗚嗚嗚 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

由於使用static 、final修飾TheadLocal對象實例, 導致了咱們這個被 ThreadLocalMap中Entry的Key所引用的ThreadLocal對象實例,一直存在強引用。

這裏有一個嚴重後果,這個 使用static 、final修飾TheadLocal對象實例 一直不會被GC,一直存在,一直存在。

TheadLocal對象實例存在強引用,會導致三個徹底失效:

導致JDK解決key內存泄露問題的弱引用清理方式徹底失效。

導致JDK解決value內存泄露問題的兩大清理方式徹底失效。

  • 探測式清理(Proactive Cleanup) 徹底失效
  • 啓發式清理(Heuristic Cleanup)徹底失效 。

這使得Thread實例內部的ThreadLocalMap中Entry的Key在Thread實例的生命期內將始終保持爲非null,從而導致Key所在的Entry不會被自動清空,這就會導致Entry中的Value指向的對象一直存在強引用,Value指向的對象在線程生命期內不會被釋放,最終導致內存泄露。

所以,使用static 、final修飾TheadLocal實例,使用完後必須使用remove()進行手動釋放。

如果使用線程池,可以定製線程池的afterExecute方法(任務執行完成之後的鉤子方法),在任務執行完成之後,調用TheadLocal實例的remove()方法對其手動釋放,從而實現的其線程內部的Entry得到釋放,參考的代碼如下:

    //線程本地變量,用於記錄線程異步任務的開始執行時間
  private static final ThreadLocal<Long> START_TIME= new ThreadLocal<>();
     ExecutorService pool = new ThreadPoolExecutor(2,
            4, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(2)) {
           ...省略其他        
            //異步任務執行完成之後的鉤子方法
            @Override
            protected void afterExecute(Runnable target, Throwable t)
            {
              ...省略其他        
                //清空TheadLocal實例的本地值
                startTime.remove();
            }
  };

ThreadLocal升級版1:InheritableThreadLocal 可繼承本地變量

什麼是可繼承本地變量InheritableThreadLocal(ITL)?

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

InheritableThreadLocal的基本使用

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

InheritableThreaLocal的原理分析

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

InheritableThreaLocal所帶來的問題

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

ThreadLocal升級版2:TransmittableThreadLocal 可透傳本地變量

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

什麼是TransmittableThreadLocal(TTL)?

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

TTL 使用場景

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

TransmittableThreadLocal的原理分析

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

ThreadLocal、InheritableThreaLocal與TransmittableThreadLocal的比較

ThreadLocalInheritableThreadLocalTransmittableThreadLocal在Java中都是用於處理線程局部變量的工具,但它們在使用場景和特性上有所不同。

  1. ThreadLocal

    • 定義:ThreadLocal是Java中一個非常重要的線程技術,它爲每個線程提供了它自己的變量副本,使得線程間無法相互訪問對方的變量,從而避免了線程間的競爭和數據泄露問題。
    • 特性
      • 使用場景:適用於需要在線程內部存儲和獲取數據,且不希望與其他線程共享數據的場景。
      • 變量存儲:提供了一種在線程內部存儲變量的機制,每個線程可以獨立地改變自己的副本,而不會影響到其他線程。
      • 線程隔離:通過爲每個線程創建變量副本,ThreadLocal實現了線程間的數據隔離,提高了多線程程序的性能。
    • 變量連續性:當線程切換時,ThreadLocal可以保持變量的連續性。
  2. InheritableThreadLocal

    • 定義:InheritableThreadLocal是ThreadLocal的一個子類,它包含了ThreadLocal的所有功能,並擴展了ThreadLocal的功能。
    • 特性
      • 線程繼承:允許父線程中的InheritableThreadLocal變量的值被子線程繼承。當創建一個新的線程時,這個新線程可以訪問其父線程中InheritableThreadLocal變量的值。
    • 使用場景:適用於需要在父線程和子線程之間傳遞數據的場景,如線程池中的任務傳遞等。
  3. TransmittableThreadLocal

    • 定義:TransmittableThreadLocal是阿里巴巴開源的一個框架,用於解決在使用線程池等場景下,ThreadLocal變量無法跨線程傳遞的問題。
    • 特性
      • 跨線程傳遞:能夠在多線程傳遞中保持變量的傳遞性,確保在父線程和子線程之間正確傳遞ThreadLocal變量。
      • copy 方法用於定製 任務提交給線程池時 的 ThreadLocal 值傳遞到 任務執行時 的拷貝行爲,缺省傳遞的是引用。注意:如果跨線程傳遞了對象引用因爲不再有線程封閉,與 InheritableThreadLocal.childValue 一樣,使用者/業務邏輯要注意傳遞對象的線程安全。
      • protected 的 beforeExecute/afterExecute 方法執行任務(Runnable/Callable)的前/後的生命週期回調。
    • 使用場景:適用於需要在線程池等場景下跨線程傳遞ThreadLocal變量的場景。

    總結起來如下

  • ThreadLocal適用於線程內部的數據存儲和訪問,確保數據在線程間的隔離。
  • InheritableThreadLocal適用於需要在父線程和子線程間傳遞數據的場景,實現數據的繼承。
  • TransmittableThreadLocal則是爲了解決在使用線程池等場景下,ThreadLocal變量無法跨線程傳遞的問題,實現數據的跨線程傳遞。

在選擇使用哪個類時,應根據具體的業務場景和需求進行權衡。同時,也需要注意在使用完這些類後,及時清理不再需要的數據,避免內存泄漏。

ThreadLocal和synchronized之間的比較

ThreadLocal和synchronized在Java中都是用於處理多線程問題的機制,但它們之間存在一些關鍵的區別。

  1. 核心思想

    • ThreadLocal:其核心思想是以空間換時間。它爲每個線程提供了一個獨立的變量副本,使得每個線程都可以訪問和修改自己的變量副本,而不會影響到其他線程。由於每個線程操作的是自己的變量副本,因此多個線程可以同時訪問該變量,且相互之間不會產生影響。這種機制主要用於保存線程私有數據、提高性能、管理線程特定的資源等場景。
    • synchronized:其核心思想是以時間換空間。它確保同一時刻只有一個線程能夠執行被synchronized修飾的代碼塊或方法,其他線程必須等待鎖的釋放。多個線程訪問的是同一個變量,當多個線程同時訪問該變量時,需要搶佔鎖,並且等待獲取鎖的線程釋放鎖,因此會消耗較多的時間。synchronized主要用於保護共享資源,防止競態條件和數據不一致問題。
  2. 應用場景

    • ThreadLocal主要用於線程間的數據隔離,常見應用場景包括線程封閉(將對象封閉在單個線程中,避免線程安全問題)、保存線程上下文信息(如在Web開發中存儲用戶信息和請求參數)、數據庫連接管理(確保每個線程獲取到自己的數據庫連接)以及線程池中的任務隔離等。
    • Synchronized主要用於線程間的數據共享,常用於保護共享資源,如共享數據或對象,確保同一時間只有一個線程訪問這些資源。此外,它還可以用於保護需要原子性執行的代碼塊,防止多線程併發執行導致的問題。
  3. 性能和資源消耗

    • ThreadLocal爲每個線程創建變量副本,因此會消耗較多的內存。但由於線程間互不干擾,所以併發性能較高。
    • synchronized則通過鎖機制來控制線程對共享資源的訪問,雖然節省了內存空間,但在多線程環境下可能會因爲鎖競爭而降低性能。

    綜上所述

ThreadLocal和synchronized在解決多線程訪問相同變量的衝突問題上各有其特點和適用場景。選擇使用哪種機制應根據具體的業務需求、性能要求和資源限制來決定。

FastThreadLocal (FTL)的實現原理

ThreadLocal 是一個常用的工具類,它允許我們創建線程局部變量。這意味着每個線程都可以獨立地改變自己的副本,而不會影響其他線程所持有的數據。

然而,ThreadLocal 在高併發環境下存在一些問題:

  1. 內存佔用:每個 ThreadLocal 變量都會在每個線程中持有一個獨立的副本,這可能導致大量的內存佔用。
  2. 性能開銷:創建和銷燬這些線程局部變量會帶來額外的性能開銷。

Netty 是一個追求極致高性能的組件, Netty 的 FastThreadLocal 就是爲了解決這些問題而誕生的。

什麼是FastThreadLocal (FTL)?

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

FastThreadLocal 如何使用

`

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

FastThreadLocal 的優勢

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

FastThreadLocal 爲什麼快

FastThreadLocal 的實現與 ThreadLocal 非常類似,Netty 爲 FastThreadLocal 量身打造了
FastThreadLocalThread 和 InternalThreadLocalMap 兩個重要的類。

下面我們看下這兩個類是如何實現的。
FastThreadLocalThread 是對 Thread 類的一層包裝,每個線程對應一個 InternalThreadLocalMap 實
例。

只有 FastThreadLocal 和 FastThreadLocalThread 組合使用時,才能發揮 FastThreadLocal 的性
能優勢。

首先看下 FastThreadLocalThread 的源碼定義:

public class FastThreadLocalThread extends Thread {
private InternalThreadLocalMap threadLocalMap;
// 省略其他代碼
}

可以看出 FastThreadLocalThread 主要擴展了 InternalThreadLocalMap 字段,

FastThreadLocalThread 主要使用 InternalThreadLocalMap 存儲數據

注意, FastThreadLocalThread 不再是使用 Thread 中的ThreadLocalMap。

所以想知道 FastThreadLocalThread 高性能的奧祕,必須要瞭解InternalThreadLocalMap 的設計原理。

上文中我們講到了 ThreadLocal 的一個重要缺點,就是 ThreadLocalMap 採用線性探測法解決 Hash衝突性能較慢,那麼 InternalThreadLocalMap 又是如何優化的呢?

首先一起看下 InternalThreadLocalMap 的內部構造。


public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {

    private static final InternalLogger logger = InternalLoggerFactory.getInstance(InternalThreadLocalMap.class);
    private static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap =
            new ThreadLocal<InternalThreadLocalMap>();
    private static final AtomicInteger nextIndex = new AtomicInteger();

    private static final int DEFAULT_ARRAY_LIST_INITIAL_CAPACITY = 8;
    private static final int ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD = 1 << 30;
    // Reference: https://hg.openjdk.java.net/jdk8/jdk8/jdk/file/tip/src/share/classes/java/util/ArrayList.java#l229
    private static final int ARRAY_LIST_CAPACITY_MAX_SIZE = Integer.MAX_VALUE - 8;
    private static final int STRING_BUILDER_INITIAL_SIZE;
    private static final int STRING_BUILDER_MAX_SIZE;
    private static final int HANDLER_SHARABLE_CACHE_INITIAL_CAPACITY = 4;
    private static final int INDEXED_VARIABLE_TABLE_INITIAL_SIZE = 32;

    public static final Object UNSET = new Object();

    /** Used by {@link FastThreadLocal} */
    private Object[] indexedVariables;

    // Core thread-locals
    private int futureListenerStackDepth;
    private int localChannelReaderStackDepth;
    private Map<Class<?>, Boolean> handlerSharableCache;
    private IntegerHolder counterHashCode;
    private ThreadLocalRandom random;
    private Map<Class<?>, TypeParameterMatcher> typeParameterMatcherGetCache;
    private Map<Class<?>, Map<String, TypeParameterMatcher>> typeParameterMatcherFindCache;

    // String-related thread-locals
    private StringBuilder stringBuilder;
    private Map<Charset, CharsetEncoder> charsetEncoderCache;
    private Map<Charset, CharsetDecoder> charsetDecoderCache;

    // ArrayList-related thread-locals
    private ArrayList<Object> arrayList;

    private BitSet cleanerFlags;

    /** @deprecated These padding fields will be removed in the future. */
    public long rp1, rp2, rp3, rp4, rp5, rp6, rp7, rp8;

    static {
        STRING_BUILDER_INITIAL_SIZE =
                SystemPropertyUtil.getInt("io.netty.threadLocalMap.stringBuilder.initialSize", 1024);
        logger.debug("-Dio.netty.threadLocalMap.stringBuilder.initialSize: {}", STRING_BUILDER_INITIAL_SIZE);

        STRING_BUILDER_MAX_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalMap.stringBuilder.maxSize", 1024 * 4);
        logger.debug("-Dio.netty.threadLocalMap.stringBuil

der.maxSize: {}", STRING_BUILDER_MAX_SIZE);
    }

// 省略其他代碼
}

從 InternalThreadLocalMap 內部實現來看,與 ThreadLocalMap 一樣都是採用數組的存儲方式。

但是InternalThreadLocalMap 並沒有使用線性探測法來解決 Hash 衝突,而是另闢蹊徑,使用數組替代map。

簡單來說,而是在 FastThreadLocal 初始化 的時候,爲每一個本地變量,分配一個全局唯一的索引 index,數組索引 index 的值採用原子類 AtomicInteger 保證順序遞增,

  • 每一個本地變量,分配一個全局唯一的索引 index .

  • 這個數組索引 index 的值 和本地變量綁定, 通過調用InternalThreadLocalMap.nextVariableIndex() 方法獲得。

然後在讀寫數據的時候通過數組下標index直接定位到 FastThreadLocal 的位置,時間複雜度爲 O(1)。

和 普通的ThreadLocalMap 相比, InternalThreadLocalMap 的 大致內部結構,如下:

假設現在我們有一批數據需要添加到數組中,分別爲 value1、value2、value3、value4,對應的
FastThreadLocal 在初始化的時候生成的數組索引分別爲 1、2、3、4。如下圖所示:

如果數組下標遞增到非常大,那麼數組也會比較大,所以 FastThreadLocal 是通過空間換時間的思想提升讀寫性能。

完整的 FastThreadLocal結構分析,下面通過一幅具體的圖,描述InternalThreadLocalMap、index 和 FastThreadLocal 之間的關係。

在這裏插入圖片描述

  1. InternalThreadLocalMap中並不是Entry的key-value結構, 而是Object數組

  2. 索引0位置存放FastThreadLocal的Set集合, 其他索引位置初始化爲UNSET, 數據存入的時候更新爲具體的Object

  3. FastThreadLocal中包含一個自增的index表示在InternalThreadLocalMap的數組中的索引位置

  4. Set<FastThreadLocal<?>>結構中存放FastThreadLocal的引用, 更容易解決內存泄漏的問題

    通過上面 FastThreadLocal 的內部結構圖,我們對比下與 ThreadLocal 有哪些區別?

  • FastThreadLocal 使用 Object 數組替代了 Entry 數組,

  • Object[0] 存儲的是一個Set集合,

  • 從數組下標 1 開始都是直接存儲的 value 數據,不再採用ThreadLocal 的鍵值對形式進行存儲。

FastThreadLocal 源碼分析

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

FastThreadLocal 的回收機制

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

FastThreadLocal 在Netty中的應用

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

和ThreadLocal相比, FastThreadLocal 的優勢:

文檔太長,超過了 平臺限制........

這部分詳細內容略,請參見PDF 《ThreadLocal 學習聖經:一次穿透TL、ITL、TTL、FTL》

說在最後:有問題找老架構取經

ThreadLocal 相關的面試題,是非常常見的面試題。

以上的內容,如果大家能對答如流,如數家珍,基本上 面試官會被你 震驚到、吸引到。最終,讓面試官愛到 “不能自已、口水直流”。offer, 也就來了。

在面試之前,建議大家系統化的刷一波 5000頁《尼恩Java面試寶典》V174,在刷題過程中,如果有啥問題,大家可以來 找 40歲老架構師尼恩交流。

另外,如果沒有面試機會,可以找尼恩來幫扶、領路。尼恩已經指導了大量的就業困難的小夥伴上岸.

前段時間,幫助一個40歲+就業困難小夥伴拿到了一個年薪100W的offer,小夥伴實現了 逆天改命

技術自由的實現路徑:

實現你的 架構自由:

喫透8圖1模板,人人可以做架構

10Wqps評論中臺,如何架構?B站是這麼做的!!!

阿里二面:千萬級、億級數據,如何性能優化? 教科書級 答案來了

峯值21WQps、億級DAU,小遊戲《羊了個羊》是怎麼架構的?

100億級訂單怎麼調度,來一個大廠的極品方案

2個大廠 100億級 超大流量 紅包 架構方案

… 更多架構文章,正在添加中

實現你的 響應式 自由:

響應式聖經:10W字,實現Spring響應式編程自由

這是老版本 《Flux、Mono、Reactor 實戰(史上最全)

實現你的 spring cloud 自由:

Spring cloud Alibaba 學習聖經》 PDF

分庫分表 Sharding-JDBC 底層原理、核心實戰(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關係(史上最全)

實現你的 linux 自由:

Linux命令大全:2W多字,一次實現Linux自由

實現你的 網絡 自由:

TCP協議詳解 (史上最全)

網絡三張表:ARP表, MAC表, 路由表,實現你的網絡自由!!

實現你的 分佈式鎖 自由:

Redis分佈式鎖(圖解 - 秒懂 - 史上最全)

Zookeeper 分佈式鎖 - 圖解 - 秒懂

實現你的 王者組件 自由:

隊列之王: Disruptor 原理、架構、源碼 一文穿透

緩存之王:Caffeine 源碼、架構、原理(史上最全,10W字 超級長文)

緩存之王:Caffeine 的使用(史上最全)

Java Agent 探針、字節碼增強 ByteBuddy(史上最全)

實現你的 面試題 自由:

4800頁《尼恩Java面試寶典 》 40個專題

免費獲取11個技術聖經PDF:

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