執行緒重用問題–ThreadLocal數據錯亂

前言

復現Java業務開發常見錯誤100例–1

項目完整程式碼:Github地址

知識點回顧:

ThreadLocal的定義和使用:

ThreadLocal概念以及使用場景

配置文件的讀取:

獲取配置文件中的key和value;

  1. 創建屬性對象
  2. 獲取文件流,並進行載入
  3. 遍歷文件流獲得屬性key和value
  4. 屬性賦值
Properties p=new Properties();
InputStream stream = clazz.getClassLoader().getResourceAsStream(fileName);
p.load(stream);
p.forEach((k,v)->{
    log.info("{}={}",k,v);
    System.setProperty(k.toString(),v.toString());
});

問題復現

問題描述:程式碼使用ThreadLocal後,有時獲取的用戶資訊是別人的。

before:是沒有傳遞值是獲取ThreadLocal中的數據;設置用戶資訊之前先查詢一次ThreadLocal中的用戶資訊
after:是設置ThreadLocal中的值後輸出的;設置用戶資訊之後再查詢一次ThreadLocal中的用戶資訊
由第二個圖可以看到before的數據本應該為null,但是現在取的是第一次塞的值1

復現過程

各位可以思考下,接下來進行復現過程:
程式碼思路比較簡單:

  1. 創建SpringBoot項目,實現controller層
  2. 創建ThreadLocal對象
  3. 對ThreadLocal賦值前,獲取執行緒資訊和用戶值
  4. 對ThreadLocal賦值
  5. 對ThreadLocal賦值後,獲取執行緒資訊和用戶值
  6. 兩者比較即可
  7. 啟動前需要讀取配置文件(注意點)

程式碼如下:

/**
 * @author xbhog
 * @describe:
 * @date 2022/8/10
 */

@RestController
@RequestMapping("threadlocal")
public class ThreadLocalDemo {
    private static final ThreadLocal<Integer> CURRENT_USER = new ThreadLocal<Integer>();
    @GetMapping("wrong")
    public Map Wrong(@RequestParam("userId") Integer userId){
        //設置用戶資訊之前先查詢一次ThreadLocal中的用戶資訊
        String before = Thread.currentThread().getName() + ":" + CURRENT_USER.get();
        //設置ThreadLocal中的用戶數據
        CURRENT_USER.set(userId);
        //設置用戶資訊之後再查詢一次ThreadLocal中的用戶資訊
        String after = Thread.currentThread().getName() + ":" + CURRENT_USER.get();
        //匯總兩次的執行結果輸出
        Map result = new HashMap();
        result.put("before",before);
        result.put("after",after);
        return result;
    }
}

按理說設置用戶資訊之前第一次獲取的值是null,但是要意識到,程式運行在Tomcat中,執行程式的執行緒是Tomcat的工作執行緒,而其工作執行緒是基於執行緒池使用的。

由上可知,執行緒池會使用固定的幾個執行緒,一旦執行緒重用,那麼很有可能會獲得前一次或者其他用戶請求的遺留值,這時候ThreadLocal中的用戶資訊就是其他用戶的資訊。

為了方便演示,在配置文件中設置下tomcat參數,將工作執行緒池最大執行緒數設置為1,這樣始終是同一個執行緒在處理請求。

server.tomcat.max-threads=1

配置文件的載入如上,具體程式碼首行有GitHub地址,歡迎star
通過上述的分析,我們明白了出現的原因,所以只要我們在使用完後,進行刪除ThreaLocal中的數據即可。
不光可以防止數據重複,也可以防止記憶體泄露(雖然出現的概率比較小)。
正確程式碼如下:

@GetMapping("right")
    public Map Rigth(@RequestParam("userId") Integer userId){
        //設置用戶資訊之前先查詢一次ThreadLocal中的用戶資訊
        String before = Thread.currentThread().getName() + ":" + CURRENT_USER.get();
        //設置ThreadLocal中的用戶數據
        CURRENT_USER.set(userId);
        try{
            //設置用戶資訊之後再查詢一次ThreadLocal中的用戶資訊
            String after = Thread.currentThread().getName() + ":" + CURRENT_USER.get();
            //匯總兩次的執行結果輸出
            Map result = new HashMap();
            result.put("before",before);
            result.put("after",after);
            return result;
        }finally {
            //刪除ThreadLocal數據,既避免了記憶體溢出的風險也解決了數據重複的問題
            CURRENT_USER.remove();
        }
    }