ThreadLocal執行緒重用時帶來的問題

heyhy發表於2024-09-11

背景

我們都知道ThreadLocal實現了資源線上程內獨享,執行緒之間隔離
實際使用中,ThreadLocal適用於變數線上程間隔離,而在方法或類間共享的場景。比如使用者資訊,當使用者資訊需要在多個方法之間傳遞或者共享使用的時候,同時,每個Tomcat請求的使用者資訊是私有的。這時可使用ThreadLocal,即直接從執行緒的ThreadLocal中獲取使用者資訊,而不用在方法的形參中獲取。
但是,不當的使用也會帶來一些問題,比如線上程池中使用ThreadLocal可能會導致獲取到ThreadLocal中的歷史資料,造成資料錯亂。如Tomcat中使用的多執行緒是執行緒池實現的,如果不當使用ThreadLocal, 由於執行緒重用,就會有該資料錯亂問題。本篇博文會舉例說明執行緒重用時,ThreadLocal變數會造成什麼影響,並給出合適的解決方案。

案例說明

使用 Spring Boot 建立一個 Web 應用程式,使用 ThreadLocal 存放一個 Integer 的值,來暫且代表需要線上程中儲存的使用者資訊,這個值初始是 null。
在業務邏輯中,我先從 ThreadLocal 獲取一次值,然後把外部傳入的引數設定到 ThreadLocal 中,來模擬從當前上下文獲取到使用者資訊的邏輯,隨後再獲取一次值,最後輸出兩次獲得的值和執行緒名稱。

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping(value = "threadlocal")
public class ThreadLocalTestClass {

    private ThreadLocal<Integer> currentUser =  ThreadLocal.withInitial(() -> null);

    @GetMapping(value = "wrong")
    public Map wrong(@RequestParam("id") Integer userId) {
        // 設定使用者資訊之前先查詢一次ThreadLocal中的使用者資訊
        String before  = Thread.currentThread().getName() + ":" + currentUser.get();
        // 設定使用者資訊到ThreadLocal
        currentUser.set(userId);
        // 設定使用者資訊之後,再查詢一次ThreadLocal中的使用者資訊
        
        String after  = Thread.currentThread().getName() + ":" + currentUser.get();
        // 彙總輸出兩次查詢結果
        Map result = new HashMap();
        result.put("before", before);
        result.put("after", after);
        return result;  
    }
}

執行結果

圖1

圖2

圖2的正確結果應該是 null, 2。但是此時卻訪問到了請求1的使用者資訊。並未實現請求1和請求2的隔離性!

執行結果說明

 * 以上方法中我們將tomcat的最大執行緒數設定為1。故只有一個執行緒在工作,所以執行緒會重用。
 * 由於我們使用了ThreadLocal, 每次請求完以後,執行緒中存放的ThreadLocal設定的值(本例是使用者資訊)依然存在。
 * 所以就可能導致另一個使用者的請求打進來以後,從ThreadLocal中獲取到的使用者資訊是上一個使用者的資訊。
 * 解決辦法比較簡單,就是在程式碼的 finally 程式碼塊中,顯式清除 ThreadLocal中的資料。
 * 這樣一來,新的請求過來即使使用了之前的執行緒也不會獲取到錯誤的使用者資訊了。

配置檔案

正確的程式碼寫法

 @GetMapping(value = "right")
    public Map wrong(@RequestParam("id") Integer userId) {
        // 設定使用者資訊之前先查詢一次ThreadLocal中的使用者資訊
        String before  = Thread.currentThread().getName() + ":" + currentUser.get();
        // 設定使用者資訊到ThreadLocal
        currentUser.set(userId);
        // 設定使用者資訊之後,再查詢一次ThreadLocal中的使用者資訊
        try {
            String after  = Thread.currentThread().getName() + ":" + currentUser.get();
            // 彙總輸出兩次查詢結果
            Map result = new HashMap();
            result.put("before", before);
            result.put("after", after);
            return result;
        } finally {
            // 返回設定的新值以後,將Threadlocal中的使用者資訊清除,防止被下一個請求訪問到,造成髒資料。
            currentUser.remove();
        }
    }

正確的結果


這樣的話,即使執行緒重用了,每個Tomcat請求也不會訪問到其他請求設定的值了,就不會出現資料錯亂問題。

相關文章