什麼,你的ThreadLocal內存泄漏了?

微信公衆號:IT一刻鐘
大型現實非嚴肅主義現場
一刻鐘與你分享優質技術架構與見聞,做一個有劇情的程序員
關注可第一時間瞭解更多精彩內容,定期有福利相送喲。

又是一個風和日麗的早上。
這天小美遇到了一個難題。


原來小美在做用戶服務鑑權的時候,需要根據每個請求獲取token:

//獲取認證信息
Authentication authentication = 
tokenProvider.getAuthentication(jwt);
//設置認證信息
SecurityContext.setAuthentication(authentication);

然後經過層層的調用,在業務代碼里根據認證信息進行權限的判斷,也就是鑑權。
小美心裏琢磨着,如果每個方法參數中都傳遞SecurityContext信息,就顯的太過冗餘,而且看着也醜陋。
那麼怎麼才能隱式傳遞參數呢?
這個當然難不倒小美,她決定用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,這樣當一個線程緣起生滅的時候,這個值會貫穿始終。
完美,小美喜滋滋的提交了代碼,然後發佈出去了。
結果第二天系統就出現異常了,明明是這個用戶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
輸出則正常。

好了,本期就說到這裏,轉發加關注,是我分享的最大動力~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章