ThreadLocal 原理和使用場景分析

風的姿態發表於2018-04-02

ThreadLocal 不知道大家有沒有用過,但至少聽說過,今天主要記錄一下 ThreadLocal 的原理和使用場景。

使用場景

直接定位到 ThreadLocal 的原始碼,可以看到原始碼註釋中有很清楚的解釋:它是執行緒的區域性變數,這些變數只能在這個執行緒內被讀寫,在其他執行緒內是無法訪問的。 ThreadLocal 定義的通常是與執行緒關聯的私有靜態欄位(例如,使用者ID或事務ID)。

變數有區域性的還有全域性的,區域性變數沒什麼好說的,一涉及到全域性,那自然就會出現多執行緒的安全問題,要保證多執行緒安全訪問,不出現髒讀髒寫,那就要涉及到執行緒同步了。而 ThreadLocal 相當於提供了介於區域性變數與全域性變數中間的這樣一種執行緒內部的全域性變數。

總結了半天,發現使用場景說到底就概括成一個:就是當我們只想在本身的執行緒內使用的變數,可以用 ThreadLocal 來實現,並且這些變數是和執行緒的生命週期密切相關的,執行緒結束,變數也就銷燬了。

所以說 ThreadLocal 不是為了解決執行緒間的共享變數問題的,如果是多執行緒都需要訪問的資料,那需要用全域性變數加同步機制。

舉幾個例子說明一下:

1、比如執行緒中處理一個非常複雜的業務,可能方法有很多,那麼,使用 ThreadLocal 可以代替一些引數的顯式傳遞;

2、比如用來儲存使用者 Session。Session 的特性很適合 ThreadLocal ,因為 Session 之前當前會話週期內有效,會話結束便銷燬。我們先籠統但不正確的分析一次 web 請求的過程:

  • 使用者在瀏覽器中訪問 web 頁面;
  • 瀏覽器向伺服器發起請求;
  • 伺服器上的服務處理程式(例如tomcat)接收請求,並開啟一個執行緒處理請求,期間會使用到 Session ;
  • 最後伺服器將請求結果返回給客戶端瀏覽器。

從這個簡單的訪問過程我們看到正好這個 Session 是在處理一個使用者會話過程中產生並使用的,如果單純的理解一個使用者的一次會話對應服務端一個獨立的處理執行緒,那用 ThreadLocal 在儲存 Session ,簡直是再合適不過了。但是例如 tomcat 這類的伺服器軟體都是採用了執行緒池技術的,並不是嚴格意義上的一個會話對應一個執行緒。並不是說這種情況就不適合 ThreadLocal 了,而是要在每次請求進來時先清理掉之前的 Session ,一般可以用攔截器、過濾器來實現。

3、在一些多執行緒的情況下,如果用執行緒同步的方式,當併發比較高的時候會影響效能,可以改為 ThreadLocal 的方式,例如高效能序列化框架 Kyro 就要用 ThreadLocal 來保證高效能和執行緒安全;

4、還有像執行緒內上線文管理器、資料庫連線等可以用到 ThreadLocal;

使用方式

ThreadLocal 的使用非常簡單,最核心的操作就是四個:建立、建立並賦初始值、賦值、取值。

1、建立

ThreadLocal<String> mLocal = new ThreadLocal<>();

2、建立並賦初值。下面程式碼表示建立了一個 String 型別的 ThreadLocal 並且重寫了 initialValue 方法,並返回初始字串,之後呼叫 get() 方法獲取的值便是initialValue 方法返回的值。

ThreadLocal<String> mLocal = new ThreadLocal<String>(){
            @Override
            protected String initialValue(){
                return "init value";
            }
        };
System.out.println(mLocal.get());

3、設定值

 mLocal.set("hello");

4、取值

mLocal.get()

實現原理

首先 ThreadLocal 是一個泛型類,保證可以接受任何型別的物件。

因為一個執行緒內可以存在多個 ThreadLocal 物件,所以其實是 ThreadLocal 內部維護了一個 Map ,這個 Map 不是直接使用的 HashMap ,而是 ThreadLocal 實現的一個叫做 ThreadLocalMap 的靜態內部類。而我們使用的 get()、set() 方法其實都是呼叫了這個 ThreadLocalMap 類對應的 get()、set() 方法。例如下面的 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);
    }

呼叫 ThreadLocal 的 set 方法時,首先獲取到了當前執行緒,然後獲取當前執行緒維護的 ThreadLocalMap 物件,最後在ThreadLocalMap 例項中新增上。如果 ThreadLocalMap 例項不存在則初始化並賦初始值。

這裡看到 set 方法的第一個引數是 thisthis即指的是當前的 ThreadLocal 物件,會看上看的程式碼就是指的 mLocal 這個物件。而在 ThreadLocalMap 的 set 方法中會根據當前 ThreadLocal 物件例項,做一些操作和判斷,最終實現賦值操作(具體參考原始碼)。

所以說,最終的變數是放在了當前執行緒的 ThreadLocalMap 中,並不是存在 ThreadLocal 上,ThreadLocal 可以理解為只是一箇中間工具,傳遞了變數值。

記憶體洩漏問題

實際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,弱引用的特點是,如果這個物件只存在弱引用,那麼在下一次垃圾回收的時候必然會被清理掉。

所以如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap 中使用這個 ThreadLocal 的 key 也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現 key 為 null 的 value。

ThreadLocalMap 實現中已經考慮了這種情況,在呼叫 set()、get()、remove() 方法的時候,會清理掉 key 為 null 的記錄。如果說會出現記憶體洩漏,那只有在出現了 key 為 null 的記錄後,沒有手動呼叫 remove() 方法,並且之後也不再呼叫 get()、set()、remove() 方法的情況下。

最後

  • 使用 ThreadLocal 的時候,最好不要宣告為靜態的;
  • 使用完 ThreadLocal ,最好手動呼叫 remove() 方法,例如上面說到的 Session 的例子,如果不在攔截器或過濾器中處理,不僅可能出現記憶體洩漏問題,而且會影響業務邏輯;

更多文章請關注我的公眾號:古時的風箏
掃碼關注

相關文章