微信公衆號:IT一刻鐘。大型現實非嚴肅主義現場,一刻鐘與你分享優質技術架構與見聞,作一個有劇情的程序員。 關注可第一時間瞭解更多精彩內容,按期有福利相送喲。程序員
又是一個風和日麗的早上。數據庫
這天小美遇到了一個難題。數組
原來小美在作用戶服務鑑權的時候,須要根據每一個請求獲取token:微信
//獲取認證信息 Authentication authentication = tokenProvider.getAuthentication(jwt); //設置認證信息 SecurityContext.setAuthentication(authentication);
而後通過層層的調用,在業務代碼里根據認證信息進行權限的判斷,也就是鑑權。多線程
小美內心琢磨着,若是每一個方法參數中都傳遞SecurityContext信息,就顯的太過冗餘,並且看着也醜陋。架構
那麼怎麼才能隱式傳遞參數呢?ide
這個固然難不倒小美,她決定用ThreadLocal來傳遞這個變量:線程
class SecurityContextHolder { private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<SecurityContext>(); public SecurityContext getContext() { SecurityContext ctx = contextHolder.get(); if (ctx == null) { contextHolder.set(createEmptyContext()); } return ctx; } } ......(省略沒必要要的) SecurityContextHolder.getContext().setAuthentication(authentication);
總體思路上就是將SecurityContext放入ThreadLocal,這樣當一個線程緣起生滅的時候,這個值會貫穿始終。設計
完美,小美喜滋滋的提交了代碼,而後發佈出去了。code
結果次日系統就出現異常了,明明是這個用戶A的發起的請求,到了數據庫中,卻發現是操做人是用戶B的信息,一時間權限大亂。
完蛋了。。。
這是爲何呢?
咱們得先扯一扯ThreadLocal,Thread,ThreadLocalMap之間的愛恨情仇。
圖片解說:
1.Thread即線程,內部有一個ThreadLocal.ThreadLocalMap,key值是ThreadLocal,value值是指定的變量值;
2.ThreadLocalMap內部有一個Entry數組,用來存儲K-V值,之因此是數組,而不是一個Entry,是由於一個線程可能對應有多個ThreadLocal;
3.ThreadLocal對象在線程外生成,多線程共享一個ThreadLocal對象,生成時需指定數據類型<?>,每一個ThreadLocal對象都自定義了不一樣的threadLocalHashCode;
4.ThreadLocal.set 首先根據當前線程Thread找到對應的ThreadLocalMap,而後將ThreadLocal的threadLocalHashCode轉換爲ThreadLocalMap裏的Entry數組下標,並存放數據於Entry[]中;
5.ThreadLocal.get 首先根據當前線程Thread找到對應的ThreadLocalMap,而後將ThreadLocal的threadLocalHashCode轉換爲ThreadLocalMap裏的Entry數組下標,根據下標從Entry[]中取出對應的數據;
6.因爲Thread內部的ThreadLocal.ThreadLocalMap對象是每一個線程私有的,因此作到了數據獨立。
因而咱們知道了ThreadLocal是如何實現線程私有變量的。 可是問題來了,若是線程數不少,一直往ThreadLocalMap中存值,那內存豈不是要撐死了?
固然不是,設計者使用了弱引用來解決這個問題:
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
不過這裏的弱引用只是針對key。每一個key都弱引用指向ThreadLocal。當把ThreadLocal實例置爲null之後,沒有任何強引用指向ThreadLocal實例,因此ThreadLocal將會被GC回收。
然而,value不能被回收,由於當前線程存在對value的強引用。只有當前線程結束銷燬後,強引用斷開,全部值纔將所有被GC回收,由此可推斷出,只有這個線程被回收了,ThreadLocal以及value纔會真正被回收。
聽起來很正常?
那若是咱們使用線程池呢?常駐線程不會被銷燬。這就完蛋了,ThreadLocal和value永遠沒法被GC回收,形成內存泄漏那是必然的。
而咱們的請求進入到系統時,並非一個請求生成一個線程,而是請求先進入到線程池,再由線程池調配出一個線程進行執行,執行完畢後放回線程池,這樣就會存在一個線程屢次被複用的狀況,這就產生了這個線程這次操做中獲取到了上次操做的值。
怎麼辦呢?
解決辦法就是每次使用完ThreadLocal對象後,都要調用其remove方法,清除ThreadLocal中的內容。 示例:
public class ThreadLocalTest { static ThreadLocal<AtomicInteger> sequencer = ThreadLocal.withInitial(() -> new AtomicInteger(0)); static class Task implements Runnable { @Override public void run() { int initial = sequencer.get().getAndIncrement(); // 指望初始爲0 System.out.println(initial); } } public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(2); executor.execute(new Task()); executor.execute(new Task()); executor.execute(new Task()); executor.execute(new Task()); executor.execute(new Task()); executor.execute(new Task()); executor.shutdown(); } }
輸出:
0
1
0
2
3
1
這裏就是錯誤的。 若是每次執行完調用remove:
@Override public void run() { int initial = sequencer.get().getAndIncrement(); // 指望初始爲0 System.out.println(initial); sequencer.remove(); }
輸出:
0
0
0
0
0
0
輸出則正常。
好了,本期就說到這裏,轉發加關注,是我分享的最大動力~