背景
在測試環境上遇到一個詭異的問題,部分業務邏輯會記錄用戶ID到數據庫,但記錄的數據會串,比如當前用戶的操作記錄會被其他用戶覆蓋, 而且這個現象是每次重啓後一小段時間內就正常
問題
在線程池內部使用了InheritableThreadLocal獲取用戶信息由於沒有及時remove,線程複用後,拿到的是舊的用戶數據
排查過程
1.通過臨時打印日誌,確認整個鏈路中用戶ID是否一致
2.確認寫日誌方法是否有被修改,最後確認是寫日誌這塊開了線程池後導致問題
思考
1.爲什麼在之前測試過程沒有復現
之前依賴的日誌記錄二方jar包進行過一次升級,原先的版本在記錄日誌沒有線程池去異步記錄,後面升級版本後,引入了線程池,而線程池是一直複用core 數量的線程的,處理完任務之後並不會回收,而InheritableThreadLocal是和線程綁在一起的,所以下一個任務複用線程池的時候用 threadLocal.get() 方法拿到的是還是老的變量
2.分析爲什麼用了InheritableThreadLocal還會導致這個數據錯亂(覆蓋)
ThreadLocal 在 ThreadLocalMap 中是以一個弱引用身份被Entry中的Key引用的,因此如果ThreadLocal沒有外部強引用來引用它,那麼ThreadLocal會在下次JVM垃圾收集時被回收。這個時候就會出現Entry中Key已經被回收,出現一個null Key的情況,外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。因此如果當前線程的生命週期很長(在本篇案例中,由於線程池的核心線程沒有被回收,一直存在),那麼其內部的ThreadLocalMap對象也一直生存下來,這些null key就存在一條強引用鏈的關係一直存在:Thread --> ThreadLocalMap-->Entry-->Value,這條強引用鏈會導致Entry不會回收,Value也不會回收
3.本地求證
爲此我寫了一個demo在本地去復現這個問題,爲了能模擬測試環境,我設置了2個核心線程,並且調用4次
根據上面的運行日誌結果,可以發現第二次的設值並沒有真正改變,所以本地模擬問題成功,接下來就是分析爲什麼會出現這個原因,以及如何解決方案
分析線程池的核心線程是如何不被回收的,源碼分析如下(主要分析重點在於核心線程創建後是如何保證不被回收)
1.跟進execute主入口分析
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true)) //未超過核心線程數,則新增 Worker 對象,true表示核心線程
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
由上可知核心線程通過addWorker方法創建
2.跟進addWorker方法
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start(); //執行核心線程
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
由上可知核心線程通過 t.start()執行,接下來繼續查看源碼看一下創建好後或者執行後是如何保證不被回收
3.跟進Work類裏面的run方法
由上可知通過While去不斷輪詢getTask方法來保證核心線程被創建後不被回收,至此也搞明白了爲什麼InheritableThreadLocal設置的value不會被remove
總結
1.每次用完後ThreadLocal後及時remove
2.儘量不要在線程池裏使用 ThreadLocal,很多時候開發關注的點比較多,疏忽的過程也在所難免,例如本次解決方案是我先把用戶ID在線程池調用前查詢出來,在傳進去