線程重用問題--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();
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章