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

前言

         又是一個風和日立的早上,這天小美遇到了一個難題:java

               

    原來是小美在作服務鑑權的時候,須要根據每一個請求獲取token:數據庫

//獲取認證信息
Authenticationauthentication = tokenProvider.getAuthentication(jwt);

//設置認證信息
SecurityContext.setAuthentication(authentication);

     而後通過層層的調用,在業務代碼里根據認證信息進行權限的判斷,也就是鑑權。小美內心琢磨着,若是每一個方法參數中都傳遞SecurityContext信息,就顯的太過冗餘,並且看着也醜陋。那麼怎麼才能隱式傳遞參數呢?這個固然難不倒小美,她決定用ThreadLocal來傳遞這個變量:數組

classSecurityContextHolder{
private static final ThreadLocal<SecurityContext>contextHolder = newThreadLocal<SecurityContext>();
  public SecurityContextgetContext(){
     SecurityContextctx = contextHolder.get();
      if(ctx==null){
         contextHolder.set(createEmptyContext());
      }
        returnctx;
    }
}

......(省略沒必要要的)多線程

SecurityContextHolder.getContext().setAuthentication(authentication);ide

總體思路上就是將SecurityContext放入ThreadLocal,這樣當一個線程緣起生滅的時候,這個值會貫穿始終。
完美,小美喜滋滋的提交了代碼,而後發佈出去了。
結果次日系統就出現異常了,明明是這個用戶A的發起的請求,到了數據庫中,卻發現是操做人是用戶B的信息,一時間權限大亂。
完蛋了。。。spa

這是爲何呢?線程

咱們得先扯一扯ThreadLocal,Thread,ThreadLocalMap之間的愛恨情仇。設計

圖片解說:3d

  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對象是每一個線程私有的,因此作到了數據獨立。code

    因而咱們知道了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(()->newAtomicInteger(0));

   static class Task implements Runnable{

   @Override

   public void run(){

     int value = sequencer.get().getAndIncrement();

     System.out.println("-------"+value);

   }

}

public static void main(String[]args){

  ExecutorService executor = Executors.newFixedThreadPool(2);

  executor.execute(newTask());

  executor.execute(newTask());

  executor.execute(newTask());

  executor.execute(newTask());

  executor.execute(newTask());

  executor.execute(newTask());

  executor.shutdown();

  }

}
輸出:
0
1
0
2
3
1

這個就是錯誤的,正確代碼以下:

public class ThreadLocalTest{

static ThreadLocal<AtomicInteger> sequencer = ThreadLocal.withInitial(()->newAtomicInteger(0));

   static class Task implements Runnable{

   @Override

   public void run(){

     int value = sequencer.get().getAndIncrement();

     System.out.println("-------"+value);
     
     sequencer.remove();

   }

}

public static void main(String[]args){

  ExecutorService executor = Executors.newFixedThreadPool(2);

  executor.execute(newTask());

  executor.execute(newTask());

  executor.execute(newTask());

  executor.execute(newTask());

  executor.execute(newTask());

  executor.execute(newTask());

  executor.shutdown();

  }

}
相關文章
相關標籤/搜索