1. 程式人生 > >什麼,你的ThreadLocal記憶體洩漏了?

什麼,你的ThreadLocal記憶體洩漏了?

前言

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

               

    原來是小美在做服務鑑權的時候,需要根據每個請求獲取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);

整體思路上就是將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(()->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();

  }

}