ThreadLocal 的開發應用

作者:KerryWu

來源:SegmentFault 思否社區

ThreadLocal 是線程私有的局部變量存儲容器,可以理解成每個線程都有自己專屬的存儲容器,用來存儲線程私有變量。ThreadLocal 在日常開發框架中應用廣泛,但用不好也會出現各種問題,本文就此講解一下。

1. 應用場景

ThreadLocal 的常見應用場景有兩種:

  1. 多線程併發場景中,用來保障線程安全。

  2. 處理較爲複雜的業務時,使用 ThreadLocal 代替參數的顯示傳遞。

1.1. 保障線程安全

多線程訪問同一個共享變量的時候容易出現併發問題,特別是多個線程對一個變量進行寫入的時候,爲了保證線程安全,一般使用者在訪問共享變量的時候需要進行額外的同步措施才能保證線程安全性,如:synchronized、Lock 之類的鎖。

ThreadLocal 是除了加鎖這種同步方式之外的一種,規避多線程訪問出現線程不安全的方法。當我們在創建一個變量後,如果每個線程對其進行訪問的時候訪問的都是線程自己的變量,這樣就不會存在線程不安全問題。

ThreadLocal 是 JDK 包提供的,它提供線程本地變量,如果創建一個 ThreadLocal 變量,那麼訪問這個變量的每個線程都會有這個變量的一個副本,在實際多線程操作的時候,操作的是自己本地內存中的變量,從而規避了線程安全問題。

1.2. 顯示傳遞參數

這裏舉幾個例子:

示例 1:獲取接口的當前請求用戶

在後臺接口業務邏輯的全過程中,如果需要在多個地方獲取當前請求用戶的信息。通常的一種做法就是:在接口請求時,通過過濾器、攔截器、AOP 等方式,從 session 或 token 中獲取當前用戶信息,存入 ThreadLocal 中。

在整個接口處理過程中,如果沒有另外創建線程,都可以直接從 ThreadLocal 變量中獲取當前用戶,而無需再從 Session、token 中驗證和獲取用戶。這種方案設計不僅提高性能,最重要的是將原本複雜的邏輯和代碼實現,變得簡潔明瞭。例如下面的這個例子:

(1)定義 ThreadLocal 變量:UserProfileThread.java

public class UserProfileThread {
    private static ThreadLocal<UserProfile> USER_PROFILE_TL =new ThreadLocal<>();

    public static void  setUserProfile(UserProfile userProfile){
        USER_PROFILE_TL.set(userProfile);
    }

    public static UserProfile getUserProfile() {
        return USER_PROFILE_TL.get();
    }

    public static String getCurrentUser() {
        return Optional.ofNullable(USER_PROFILE_TL.get())
                .map(UserProfile::getUid)
                .orElse(UserProfile.ANONYMOUS_USER);
    }
}

(2)在過濾器中設置變量值:

@Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        UserProfile userProfile = null;
        // ... 驗證和獲取用戶信息 userProfile
        UserProfileThread.setUserProfile(userProfile);
        filterChain.doFilter(servletRequest, servletResponse);
    }

(3)獲取當前用戶信息

//獲取當前用戶
String uid=UserProfileThread.getCurrentUser();
//獲取當前用戶對象
UserProfile user=UserProfileThread.getUserProfile();

示例 2:spring 框架中保證數據庫事務在同一個連接下執行

要想實現 jdbc 事務, 就必須是在同一個連接對象中操作,多個連接下事務就會不可控,需要藉助分佈式事務完成。那 spring 框架如何保證數據庫事務在同一個連接下執行的呢?

DataSourceTransactionManager 是 spring 的數據源事務管理器,它會在你調用 getConnection() 的時候從數據庫連接池中獲取一個 connection, 然後將其與 ThreadLocal 綁定,事務完成後解除綁定。這樣就保證了事務在同一連接下完成。

2. 實現原理

ThreadLocal 類提供 set/get 方法存儲和獲取 value 值,但實際上 ThreadLocal 類並不存儲 value 值,真正存儲是靠 ThreadLocalMap 這個類。

每個線程實例都對應一個 TheadLocalMap 實例,我們可以在同一個線程裏實例化很多個 ThreadLocal 來存儲很多種類型的值,這些 ThreadLocal 實例分別作爲 key,對應各自的 value,最終存儲在 Entry table 數組中。

我們看看 ThreadLocal 的 set 方法:

public class ThreadLocal<T> {
 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;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    // 省略其他方法
}

set 的邏輯比較簡單,就是獲取當前線程的 ThreadLocalMap,然後往 map 裏添加 KV,K 是當前 ThreadLocal 實例,V 是我們傳入的 value。這裏需要注意一下,map 的獲取是需要從 Thread 類對象裏面取,看一下 Thread 類的定義。

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    //省略其他
}

Thread 類維護了一個 ThreadLocalMap 的變量引用。

因此,我們可以得出如下結論:

3. 注意事項

ThreadLocal 實例有提供 remove() 方法,用於回收對象,清除對應的內存佔用。這個方法通常容易被忽略,而導致出現了各種問題。如下面幾種:

  1. 線程複用:在 “獲取接口的當前請求用戶” 的例子中,Tomcat 中是通過線程池來處理用戶請求的,而線程池中線程是複用的。肯定會出現一個線程前後被不同用戶的接口請求複用的情況,因此需要對用過的 ThreaLocal 變量進行覆蓋或清除。

  2. 內存溢出:由於 ThreadLocalMap 的生命週期跟 Thread 一樣長,如果創建的 ThreadLocal 變量很多,即對應的 key 佔用的內存很大,但卻沒有手動刪除,到了一定程度就會導致內存泄漏。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/NyfysjSd5GnpZznuSzLK7Q