什麼,你的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之間的愛恨情仇。
圖片解說:
- Thread即為執行緒,圖中有ThreadLocal.ThreadLocalMap,key為ThreadLocal,而value為指定的變數的值;
- ThreadLocalMap裡面有一個Entry[]陣列,用來儲存K-V值,之所以是陣列,而不是一個Entry,是因為一個執行緒可能對應有多個ThreadLocal;
- ThreadLocal物件線上程外生成,多執行緒共享一個ThreadLocal物件,生成時需指定資料型別,每個ThreadLocal物件都自定義了不同的threadLocalHashCode;
- ThreadLocal.set首先根據當前執行緒Thread找到對應的ThreadLocalMap,然後將ThreadLocal的threadLocalHashCode轉換為ThreadLocalMap裡的Entry陣列下標,並存放資料於Entry[]中;
- ThreadLocal.get首先根據當前執行緒Thread找到對應的ThreadLocalMap,然後將ThreadLocal的threadLocalHashCode轉換為ThreadLocalMap裡的Entry陣列下標,根據下標從Entry[]中取出對應的資料;
-
由於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();
}
}