天天看點

阿裡三面:說說線程封閉與ThreadLocal的關系(中)ThreadLocal的副作用

三個重要方法:

  • set()

    如果沒有set操作的

    ThreadLocal

    , 很容易引起髒資料問題
  • get()

    始終沒有get操作的

    ThreadLocal

    對象是沒有意義的
  • remove()

    如果沒有remove操作,則容易引起記憶體洩漏

  • 如果ThreadLocal是非靜态的,屬于某個線程執行個體,那就失去了線程間共享的本質屬性;

那麼ThreadLocal到底有什麼作用呢?

我們知道,局部變量在方法内各個代碼塊間進行傳遞,而類變量在類内方法間進行傳遞;

複雜的線程方法可能需要調用很多方法來實作某個功能,這時候用什麼來傳遞線程内變量呢?

即ThreadLocal,它通常用于同一個線程内,跨類、跨方法傳遞資料;

如果沒有ThreadLocal,那麼互相之間的資訊傳遞,勢必要靠傳回值和參數,這樣無形之中,有些類甚至有些架構會互相耦合;

通過将Thread構造方法的最後一個參數設定為true,可以把目前線程的變量繼續往下傳遞給它建立的子線程

public Thread (ThreadGroup group, Runnable target, String name,long stackSize, boolean inheritThreadLocals) [
   this (group, target, name,  stackSize, null, inheritThreadLocals) ;
}      

parent為其父線程

if (inheritThreadLocals && parent. inheritableThreadLocals != null)
      this. inheritableThreadLocals = ThreadLocal. createInheritedMap (parent. inheritableThreadLocals) ;      

createlnheritedMap()

其實就是調用

ThreadLocalMap

的私有構造方法來産生一個執行個體對象,把父線程中不為

null

的線程變量都拷貝過來

private ThreadLocalMap (ThreadLocalMap parentMap) {
    // table就是存儲
    Entry[] parentTable = parentMap. table;
    int len = parentTable. length;
    setThreshold(len) ;
    table = new Entry[len];

    for (Entry e : parentTable) {
      if (e != null) {
        ThreadLocal<object> key = (ThreadLocal<object>) e.get() ;
        if (key != null) {
          object value = key. childValue(e.value) ;
          Entry c = new Entry(key, value) ;
          int h = key. threadLocalHashCode & (len - 1) ;
          while (table[h] != null)
            h = nextIndex(h, len) ;
          table[h] = C;
          size++;
        }
    }
}      

很多場景下可通過ThreadLocal來透傳全局上下文的;

比如用ThreadLocal來存儲監控系統的某個标記位,暫且命名為traceld.

某次請求下所有的traceld都是一緻的,以獲得可以統一解析的日志檔案;

但在實際開發過程中,發現子線程裡的traceld為null,跟主線程的traceld并不一緻,是以這就需要剛才說到的InheritableThreadLocal來解決父子線程之間共享線程變量的問題,使整個連接配接過程中的traceld一緻。

示例代碼如下

import org.apache.commons.lang3.StringUtils;

/**
 * @author sss
 * @date 2019/1/17
 */
public class RequestProcessTrace {

    private static final InheritableThreadLocal<FullLinkContext> FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL
            = new InheritableThreadLocal<FullLinkContext>();

    public static FullLinkContext getContext() {
        FullLinkContext fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();
        if (fullLinkContext == null) {
            FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.set(new FullLinkContext());
            fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();
        }
        return fullLinkContext;
    }

    private static class FullLinkContext {
        private String traceId;

        public String getTraceId() {
            if (StringUtils.isEmpty(traceId)) {
                FrameWork.startTrace(null, "JavaEdge");
                traceId = FrameWork.getTraceId();
            }
            return traceId;
        }

        public void setTraceId(String traceId) {
            this.traceId = traceId;
        }
    }

}      

ThreadLocal的副作用

為了使線程安全地共享某個變量,JDK給出了ThreadLocal.

但ThreadLocal的主要問題是會産生髒資料和記憶體洩漏;

這兩個問題通常是線上程池的線程中使用ThreadLocal引發的,因為線程池有線程複用和記憶體常駐兩是線上程池的線程中使用ThreadLocal 引發的,因為線程池有線程複用和記憶體常駐兩個特點

髒資料

線程複用會産生髒資料。

由于線程池會重用 Thread 對象,與 Thread 綁定的靜态屬性 ThreadLocal 變量也會被重用。

如果在實作的線程run()方法中不顯式調用remove()清理與線程相關的ThreadLocal資訊,那麼若下一個線程不調用set(),就可能get() 到重用的線程資訊。包括ThreadLocal所關聯的線程對象的value值。

髒讀案例

比如,使用者A下單後沒有看到訂單記錄,而使用者B卻看到了使用者A的訂單記錄。通過排查發現是由于 session 優化引發。

在原來的請求過程中,使用者每次請求Server,都需要通過 sessionId 去緩存裡查詢使用者的session資訊,這樣無疑增加了一次調用。

是以工程師決定采用某架構來緩存每個使用者對應的SecurityContext,它封裝了session 相關資訊。優化後雖然會為每個使用者建立一個 session 相關的上下文,但由于Threadlocal沒有線上程處理結束時及時remove()。在高并發場景下,線程池中的線程可能會讀取到上一個線程緩存的使用者資訊。

示例代碼

阿裡三面:說說線程封閉與ThreadLocal的關系(中)ThreadLocal的副作用

輸出結果

阿裡三面:說說線程封閉與ThreadLocal的關系(中)ThreadLocal的副作用

重用錯誤案例

生産環境中,有時擷取到的使用者資訊是别人的。檢視代碼後,發現是使用了

ThreadLocal

緩存擷取到的使用者資訊。

ThreadLocal

适用于變量線上程間隔離,而在方法或類間共享的場景。

若使用者資訊的擷取比較昂貴(比如從DB查詢),則在

ThreadLocal

中緩存比較合适。

問題來了,為什麼有時會出現使用者資訊錯亂?

1.1 案例

使用Spring Boot建立一個Web應用程式,使用ThreadLocal存放一個Integer值,代表需要線上程中儲存的使用者資訊,這個值初始是null。在業務邏輯中,我先從ThreadLocal擷取一次值,然後把外部傳入的參數設定到ThreadLocal中,來模拟從目前上下文擷取到使用者資訊的邏輯,随後再擷取一次值,最後輸出兩次獲得的值和線程名稱。

阿裡三面:說說線程封閉與ThreadLocal的關系(中)ThreadLocal的副作用

固定思維認為,在設定使用者資訊前第一次擷取的值始終是null,但要清楚程式運作在Tomcat,執行程式的線程是Tomcat的工作線程,其基于線程池。

而線程池會重用固定線程,一旦線程重用,那麼很可能首次從ThreadLocal擷取的值是之前其他使用者的請求遺留的值。這時,ThreadLocal中的使用者資訊就是其他使用者的資訊。

1.2 bug 重制

在配置檔案設定Tomcat參數-工作線程池最大線程數設為1,這樣始終是同一線程在處理請求:

server.tomcat.max-threads=1      

先讓使用者1請求接口,第一、第二次擷取到使用者ID分别是null和1,符合預期

阿裡三面:說說線程封閉與ThreadLocal的關系(中)ThreadLocal的副作用

使用者2請求接口,bug複現!第一、第二次擷取到使用者ID分别是1和2,顯然第一次擷取到了使用者1的資訊,因為Tomcat線程池重用了線程。兩次請求線程都是同一線程:

http-nio-45678-exec-1

阿裡三面:說說線程封閉與ThreadLocal的關系(中)ThreadLocal的副作用

寫業務代碼時,首先要了解代碼會跑在什麼線程上:

  • Tomcat伺服器下跑的業務代碼,本就運作在一個多線程環境(否則接口也不可能支援這麼高的并發),并不能認為沒有顯式開啟多線程就不會有線程安全問題
  • 線程建立較昂貴,是以Web伺服器會使用線程池處理請求,線程會被重用。使用類似ThreadLocal工具存放資料時,需注意在代碼運作完後,顯式清空設定的資料。