ThreadLocal线程复用导致的安全问题
我們知道,ThreadLocal 適用于變量在線程間隔離,而在方法或類間共享的場景。如果用戶信息的獲取比較昂貴(比如從數(shù)據(jù)庫查詢用戶信息),那么在 ThreadLocal 中緩存數(shù)據(jù)是比較合適的做法。但,這么做為什么會出現(xiàn)用戶信息錯亂的 Bug 呢?
我們看一個具體的案例吧。
使用 Spring Boot 創(chuàng)建一個 Web 應(yīng)用程序,使用 ThreadLocal 存放一個 Integer 的值,來暫且代表需要在線程中保存的用戶信息,這個值初始是 null。在業(yè)務(wù)邏輯中,我先從 ThreadLocal 獲取一次值,然后把外部傳入的參數(shù)設(shè)置到 ThreadLocal 中,來模擬從當(dāng)前上下文獲取到用戶信息的邏輯,隨后再獲取一次值,最后輸出兩次獲得的值和線程名稱。
? ?private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);@GetMapping("wrong")public Map wrong(@RequestParam("userId") Integer userId) {//設(shè)置用戶信息之前先查詢一次ThreadLocal中的用戶信息String before = Thread.currentThread().getName() + ":" + currentUser.get();//設(shè)置用戶信息到ThreadLocalcurrentUser.set(userId);//設(shè)置用戶信息之后再查詢一次ThreadLocal中的用戶信息Map result;String after = Thread.currentThread().getName() + ":" + currentUser.get();//匯總輸出兩次查詢結(jié)果 ? ?result = new HashMap();result.put("before", before);result.put("after", after);return result;}按理說,在設(shè)置用戶信息之前第一次獲取的值始終應(yīng)該是 null,但我們要意識到,程序運(yùn)行在 Tomcat 中,執(zhí)行程序的線程是 Tomcat 的工作線程,而 Tomcat 的工作線程是基于線程池的。
顧名思義,線程池會重用固定的幾個線程,一旦線程重用,那么很可能首次從 ThreadLocal 獲取的值是之前其他用戶的請求遺留的值。這時,ThreadLocal 中的用戶信息就是其他用戶的信息。
為了更快地重現(xiàn)這個問題,我在配置文件中設(shè)置一下 Tomcat 的參數(shù),把工作線程池最大線程數(shù)設(shè)置為 1,這樣始終是同一個線程在處理請求:
server.tomcat.max-threads=1?
運(yùn)行程序后先讓用戶 1 來請求接口,可以看到第一和第二次獲取到用戶 ID 分別是 null 和 1,符合預(yù)期:
?
隨后用戶 2 來請求接口,這次就出現(xiàn)了 Bug,第一和第二次獲取到用戶 ID 分別是 1 和 2,顯然第一次獲取到了用戶 1 的信息,原因就是 Tomcat 的線程池重用了線程。從圖中可以看到,兩次請求的線程都是同一個線程:http-nio-8080-exec-1。
?
這個例子告訴我們,在寫業(yè)務(wù)代碼時,首先要理解代碼會跑在什么線程上:
我們可能會抱怨學(xué)多線程沒用,因?yàn)榇a里沒有開啟使用多線程。但其實(shí),可能只是我們沒有意識到,在 Tomcat 這種 Web 服務(wù)器下跑的業(yè)務(wù)代碼,本來就運(yùn)行在一個多線程環(huán)境(否則接口也不可能支持這么高的并發(fā)),并不能認(rèn)為沒有顯式開啟多線程就不會有線程安全問題。
因?yàn)榫€程的創(chuàng)建比較昂貴,所以 Web 服務(wù)器往往會使用線程池來處理請求,這就意味著線程會被重用。這時,使用類似 ThreadLocal 工具來存放一些數(shù)據(jù)時,需要特別注意在代碼運(yùn)行完后,顯式地去清空設(shè)置的數(shù)據(jù)。如果在代碼中使用了自定義的線程池,也同樣會遇到這個問題。
理解了這個知識點(diǎn)后,我們修正這段代碼的方案是,在代碼的 finally 代碼塊中,顯式清除 ThreadLocal 中的數(shù)據(jù)。這樣一來,新的請求過來即使使用了之前的線程也不會獲取到錯誤的用戶信息了。修正后的代碼如下:
? ?@GetMapping("right")public Map wrong(@RequestParam("userId") Integer userId) {//設(shè)置用戶信息之前先查詢一次ThreadLocal中的用戶信息String before = Thread.currentThread().getName() + ":" + currentUser.get();//設(shè)置用戶信息到ThreadLocalcurrentUser.set(userId);//設(shè)置用戶信息之后再查詢一次ThreadLocal中的用戶信息Map result;try {String after = Thread.currentThread().getName() + ":" + currentUser.get();//匯總輸出兩次查詢結(jié)果result = new HashMap();result.put("before", before);result.put("after", after);} finally {currentUser.remove();}return result;}重新運(yùn)行程序可以驗(yàn)證,再也不會出現(xiàn)第一次查詢用戶信息查詢到之前用戶請求的 Bug
總結(jié)
以上是生活随笔為你收集整理的ThreadLocal线程复用导致的安全问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java.util.Map中put,co
- 下一篇: 如何在队列排队之前让ThreadPool