前言
復現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();
}
}