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

微信公衆號: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

輸出則正常。

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

相關文章
相關標籤/搜索