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,可以進行事務回滾,提交等操作。
/** * 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方法,那麼這個期間就會發生真正的記憶體洩露。
- JVM利用設定ThreadLocalMap的Key為弱引用,來避免記憶體洩露。
- JVM利用呼叫remove、get、set方法的時候,回收弱引用。
- 當ThreadLocal儲存很多Key為null的Entry的時候,而不再去呼叫remove、get、set方法,那麼將導致記憶體洩漏。
- 當使用static ThreadLocal的時候,延長ThreadLocal的生命週期,那也可能導致記憶體洩漏。因為,static變數在類未載入的時候,它就已經載入,當執行緒結束的時候,static變數不一定會回收。那麼,比起普通成員變數使用的時候才載入,static的生命週期加長將更容易導致記憶體洩漏危機。
參考連結:https://www.cnblogs.com/aspirant/p/8991010.html