1. 程式人生 > >ThreadLocal深度解析和應用示例

ThreadLocal深度解析和應用示例

開篇明意

  ThreadLocal是JDK包提供的執行緒本地變數,如果建立了ThreadLocal<T>變數,那麼訪問這個變數的每個執行緒都會有這個變數的一個副本,在實際多執行緒操作的時候,操作的是自己本地記憶體中的變數,從而規避了執行緒安全問題。

  ThreadLocal很容易讓人望文生義,想當然地認為是一個“本地執行緒”。其實,ThreadLocal並不是一個Thread,而是Thread的一個區域性變數,也許把它命名ThreadLocalVariable更容易讓人理解一些。

  來看看官方的定義:這個類提供執行緒區域性變數。這些變數與正常的變數不同,每個執行緒訪問一個(通過它的get或set方法)都有它自己的、獨立初始化的變數副本。ThreadLocal例項通常是類中的私有靜態欄位,希望將狀態與執行緒關聯(例如,使用者ID或事務ID)。

原始碼解析

  1.核心方法之   set(T t)

 1     /**
 2      * Sets the current thread's copy of this thread-local variable
 3      * to the specified value.  Most subclasses will have no need to
 4      * override this method, relying solely on the {@link #initialValue}
 5      * method to set the values of thread-locals.
 6      *
 7      * @param value the value to be stored in the current thread's copy of
 8      *        this thread-local.
 9      */
10     public void set(T value) {
11         Thread t = Thread.currentThread();
12         ThreadLocalMap map = getMap(t);
13         if (map != null)
14             map.set(this, value);
15         else
16             createMap(t, value);
17     }

解析:

  當呼叫ThreadLocal的set(T t)的時候,程式碼首先會獲取當前執行緒的 ThreadLocalMap(ThreadLocal中的靜態內部類,同時也作為Thread的成員變數存在,後面會進一步瞭解ThreadLocalMap),如果ThreadLocalMap存在,將ThreadLocal作為map的key,要儲存的值作為value來put進map中(如果map不存在就先建立map,然後再進行put);

  2.核心方法值 get()

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
       Thread t = Thread.currentThread();
       ThreadLocalMap map = getMap(t);        //此處和set方法一致,也是通過當前執行緒獲取對應的成員變數ThreadLocalMap,map中存放的是Entry(ThreadLocalMap的內部類(繼承了弱引用))
    if (map != null) {
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) {
        @SuppressWarnings("unchecked")
        T result = (T)e.value;
        return result;
      }
    }
    return setInitialValue();
}

解析:

  剛才把物件放到set到map中,現在根據key將其取出來,值得注意的是這裡的map裡面存的可不是鍵值對,而是繼承了WeakReference<ThreadLocal<?>> 的Entry物件,關於ThreadLocalMap.Entry類,後面會有更加詳盡的講述。

核心方法之  remove()

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

解析:

  通過getMap方法獲取Thread中的成員變數ThreadLocalMap,在map中移除對應的ThreadLocal,由於ThreadLocal(key)是一種弱引用,弱引用中key為空,gc會回收變數value,看一下核心的m.remove(this);方法

        /**
         * Remove the entry for key.
         */
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1); //定義Entry在陣列中的標號
            for (Entry e = tab[i];              //通過迴圈的方式remove掉Thread中所有的Entry
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {   
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        } 

 

 

 

靈魂提問

  問:threadlocal是做什麼用的,用在哪些場景當中?  

    結合官方對ThreadLocal類的定義,threadLocal主要滿足某些變數或者示例是執行緒隔離的,但是在相同執行緒的多個類或者方法中都能使用的到,並且當執行緒結束時該變數也應該銷燬。通俗點講:ThreadLocal保證每個執行緒有自己的資料副本,當執行緒結束後可  以獨立回收。由於ThreadLocal的特性,同一執行緒在某地方進行設定,在隨後的任意地方都可以獲取到。從而可以用來儲存執行緒上下文資訊。常用的比如每個請求怎麼把一串後續關聯起來,就可以用ThreadLocal進行set,在後續的任意需要記錄日誌的方法裡面進行get獲取到請求id,從而把整個請求串起來。     使用場景有很多,比如:
  • 基於使用者請求執行緒的資料隔離(每次請求都繫結userId,userId的值存在於ThreadLoca中)
  • 跟蹤一個請求,從接收請求,處理到返回的整個流程,有沒有好的辦法   思考:微服務中的鏈路追蹤是否利用了ThreadLocal特性
  • 資料庫的讀寫分離
  • 還有比如Spring的事務管理,用ThreadLocal儲存Connection,從而各個DAO可以獲取同一Connection,可以進行事務回滾,提交等操作。
    問:如果我啟動另外一個執行緒。那麼在主執行緒設定的Threadlocal值能被子執行緒拿到嗎?     原始的ThreadLocal是不具有繼承(或者說傳遞)特性的     問:那該如何解決ThreadLocal無法傳遞的問題呢?     用ThreadLocal的子類 InheritableThreadLocal,InheritableThreadLocal是具有傳遞性的
    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

解析:因為InheritableThreadLocal重寫了ThreadLocal中的getMap 和createMap方法,這兩個方法維護的是Thread中的另外一個成員變數  inheritableThreadLocals,執行緒在建立的時候回覆制inheritableThreadLocals中的值 ;

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
  //Thread類中維護的成員變數,ThreadLocal會維護該變數
ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */
//Thread中維護的成員變數 ,InheritableThreadLocal 中維護該變數
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;


 

//Thread init方法中的關鍵程式碼       
 if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

總結

  • ThreadLocal類封裝了getMap()、Set()、Get()、Remove()4個核心方法。
  • 通過getMap()獲取每個子執行緒Thread持有自己的ThreadLocalMap例項, 因此它們是不存在併發競爭的。可以理解為每個執行緒有自己的變數副本。
  • ThreadLocalMap中Entry[]陣列儲存資料,初始化長度16,後續每次都是2倍擴容。主執行緒中定義了幾個ThreadLocal變數,Entry[]才有幾個key。
  • Entry的key是對ThreadLocal的弱引用,當拋棄掉ThreadLocal物件時,垃圾收集器會忽略這個key的引用而清理掉ThreadLocal物件, 防止了記憶體洩漏。

    tips:上面四個總結來源於其他技術部落格,個人認為總結的比較合理所以直接摘抄過來了

拓展:

  ThreadLocal線上程池中使用容易發生的問題: 記憶體洩漏,先看下圖

  

  每個thread中都存在一個map, map的型別是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal例項. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當把threadlocal例項置為null以後,沒有任何強引用指向threadlocal例項,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連線過來的強引用. 只有當前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收.

  所以得出一個結論就是隻要這個執行緒物件被gc回收,就不會出現記憶體洩露,但在threadLocal設為null和執行緒結束這段時間不會被回收的,就發生了我們認為的記憶體洩露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是執行緒物件不被回收的情況,這就發生了真正意義上的記憶體洩露。比如使用執行緒池的時候,執行緒結束是不會銷燬的,會再次使用的。就可能出現記憶體洩露。  

  PS.Java為了最小化減少記憶體洩露的可能性和影響,在ThreadLocal的get,set的時候都會清除執行緒Map裡所有key為null的value。所以最怕的情況就是,threadLocal物件設null了,開始發生“記憶體洩露”,然後使用執行緒池,這個執行緒結束,執行緒放回執行緒池中不銷燬,這個執行緒一直不被使用,或者分配使用了又不再呼叫get,set方法,那麼這個期間就會發生真正的記憶體洩露。 

 

  1. JVM利用設定ThreadLocalMap的Key為弱引用,來避免記憶體洩露。
  2. JVM利用呼叫remove、get、set方法的時候,回收弱引用。
  3. 當ThreadLocal儲存很多Key為null的Entry的時候,而不再去呼叫remove、get、set方法,那麼將導致記憶體洩漏。
  4. 當使用static ThreadLocal的時候,延長ThreadLocal的生命週期,那也可能導致記憶體洩漏。因為,static變數在類未載入的時候,它就已經載入,當執行緒結束的時候,static變數不一定會回收。那麼,比起普通成員變數使用的時候才載入,static的生命週期加長將更容易導致記憶體洩漏危機。

 

  參考連結:https://www.cnblogs.com/aspirant/p/8991010.html