執行緒重用問題–ThreadLocal數據錯亂
- 2022 年 8 月 14 日
- 筆記
- Java基礎, JUC並發編程的藝術, JVM和JUC中的問題, springboot, 閱讀片段
前言
復現Java業務開發常見錯誤100例–1
項目完整程式碼:Github地址
知識點回顧:
ThreadLocal的定義和使用:
配置文件的讀取:
獲取配置文件中的key和value;
- 創建屬性對象
- 獲取文件流,並進行載入
- 遍歷文件流獲得屬性key和value
- 屬性賦值
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
。
復現過程
各位可以思考下,接下來進行復現過程:
程式碼思路比較簡單:
- 創建SpringBoot項目,實現controller層
- 創建ThreadLocal對象
- 對ThreadLocal賦值前,獲取執行緒資訊和用戶值
- 對ThreadLocal賦值
- 對ThreadLocal賦值後,獲取執行緒資訊和用戶值
- 兩者比較即可
- 啟動前需要讀取配置文件(注意點)
程式碼如下:
/**
* @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();
}
}