開篇明意
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是做什么用的,用在哪些場景當中?
- 基於用戶請求線程的數據隔離(每次請求都綁定userId,userId的值存在於ThreadLoca中)
- 跟蹤一個請求,從接收請求,處理到返回的整個流程,有沒有好的辦法 思考:微服務中的鏈路追蹤是否利用了ThreadLocal特性
- 數據庫的讀寫分離
- 還有比如Spring的事務管理,用ThreadLocal存儲Connection,從而各個DAO可以獲取同一Connection,可以進行事務回滾,提交等操作。
/**
* 重寫Threadlocal類中的getMap方法,在原Threadlocal中是返回
* t.theadLocals,而在這么卻是返回了inheritableThreadLocals,因為
* Thread類中也有一個要保存父子傳遞的變量
*/ ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; }
/** * 同理,在創建ThreadLocalMap的時候不是給t.threadlocal賦值 *而是給inheritableThreadLocals變量賦值 * */
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方法中的關鍵代碼,簡單來說是將父類中inheritableThreadLocals中的值拷貝到當前線程的inheritableThreadLocals中(淺拷貝,拷貝的是value的地址引用)
if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
總結
- ThreadLocal類封裝了getMap()、Set()、Get()、Remove()4個核心方法。
- 通過getMap()獲取每個子線程Thread持有自己的ThreadLocalMap實例, 因此它們是不存在並發競爭的。可以理解為每個線程有自己的變量副本。
- ThreadLocalMap中Entry[]數組存儲數據,初始化長度16,后續每次都是1.5倍擴容。主線程中定義了幾個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
在線程池中使用ThreadLocal
通過上面的分析可以知道InheritableThreadLocal是通過Thread()的inint方法實現父子之間的傳遞的,但是線程池是統一創建線程並實現復用的,這樣就好導致下面的問題發生:
- 線程不會銷毀,ThreadLocal也不會被銷毀,這樣會導致ThreadLoca會隨着Thread的復用而復用
- 子線程無法通過InheritableThreadLocal實現傳遞性(因為沒有單獨的調用Thread的Init方法進行map的復制),子線程中get到的是null或者是其他線程復用的錯亂值(疑問點還沒搞清楚原因,后續補充::在異步線程中會出現null的情況,同步線程不會出現)
ps:線程池中的線程是什么時候創建的?
解決方案:
下面兩個鏈接有詳細的說明,我就不重復寫了,后續我會將本文進一般優化並添加一些例子來幫助說明,歡迎收藏,關於本文有不同的意見歡迎評論指正……
https://blog.csdn.net/hanziyuan08/article/details/78190863
https://www.cnblogs.com/sweetchildomine/p/8807059.html