0、問題
- 和Synchronized的區別
- 存盤在jvm的哪個區域
- 真的只是當前執行緒可見嗎
- 會導致記憶體泄漏么
- 為什么用Entry陣列而不是Entry物件
- 你學習的開源框架哪些用到了ThreadLocal
- ThreadLocal里的物件一定是執行緒安全的嗎
- 筆試題
一、概述
1、官方術語
ThreadLocal類是用來提供執行緒內部的區域變數,讓這些變數在多執行緒環境下訪問(get/set)時能保證各個執行緒里的變數相對獨立于其他執行緒內的變數,
2、大白話
ThreadLocal是一個關于創建執行緒區域變數的類,
通常情況下,我們創建的成員變數都是執行緒不安全的,因為他可能被多個執行緒同時修改,此變數對于多個執行緒之間彼此并不獨立,是共享變數,而使用ThreadLocal創建的變數只能被當前執行緒訪問,其他執行緒無法訪問和修改,也就是說:將執行緒公有化變成執行緒私有化,
二、應用場景
- 每個執行緒都需要一個獨享的物件(比如工具類,典型的就是
SimpleDateFormat,每次使用都new一個多浪費性能呀,直接放到成員變數里又是執行緒不安全,所以把他用ThreadLocal管理起來就完美了,)
比如:
/** * Description: SimpleDateFormat就一份,不浪費資源, * * @author TongWei.Chen 2020-07-10 14:00:29 */ public class ThreadLocalTest05 { public static String dateToStr(int millisSeconds) { Date date = new Date(millisSeconds); SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); return simpleDateFormat.format(date); } private static final ExecutorService executorService = Executors.newFixedThreadPool(100); public static void main(String[] args) { for (int i = 0; i < 3000; i++) { int j = i; executorService.execute(() -> { String date = dateToStr(j * 1000); // 從結果中可以看出是執行緒安全的,時間沒有重復的, System.out.println(date); }); } executorService.shutdown(); } } class ThreadSafeFormatter { public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); } }; // java8的寫法,裝逼神器 // public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = // ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")); }
細心的朋友已經發現了,這TM也是每個執行緒都創建一個
SimpleDateFormat啊,跟直接在方法內部new沒區別,錯了,大錯特錯!1個請求進來是一個執行緒,他可能貫穿了N個方法,你這N個方法假設有3個都在使用dateToStr(),你直接new的話會產生三個SimpleDateFormat物件,而用ThreadLocal的話只會產生一個物件,一個執行緒一個,
- 每個執行緒內需要保存全域變數(比如在登錄成功后將用戶資訊存到
ThreadLocal里,然后當前執行緒操作的業務邏輯直接get取就完事了,有效的避免的引數來回傳遞的麻煩之處),一定層級上減少代碼耦合度,
再細化一點就是:
- 比如存盤 交易id等資訊,每個執行緒私有,
- 比如aop里記錄日志需要before記錄請求id,end拿出請求id,這也可以,
- 比如jdbc連接池(很典型的一個
ThreadLocal用法) - ....等等....
三、核心知識
1、類關系
每個Thread物件中都持有一個ThreadLocalMap的成員變數,每個ThreadLocalMap內部又維護了N個Entry節點,也就是Entry陣列,每個Entry代表一個完整的物件,key是ThreadLocal本身,value是ThreadLocal的泛型值,
核心原始碼如下
// java.lang.Thread類里持有ThreadLocalMap的參考 public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null; } // java.lang.ThreadLocal有內部靜態類ThreadLocalMap public class ThreadLocal<T> { static class ThreadLocalMap { private Entry[] table; // ThreadLocalMap內部有Entry類,Entry的key是ThreadLocal本身,value是泛型值 static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } } }
2、類關系圖
ThreadLocal記憶體結構圖,

3、主要方法
initialValue:初始化,在get方法里懶加載的,get:得到這個執行緒對應的value,如果呼叫get之前沒set過,則get內部會執行initialValue方法進行初始化,set:為這個執行緒設定一個新值,remove:洗掉這個執行緒對應的值,防止記憶體泄露的最佳手段,
3.1、initialValue
3.1.1、什么意思
見名知意,初始化一些value(泛型值),懶加載的,
3.1.2、觸發時機
呼叫get方法之前沒有呼叫set方法,則get方法內部會觸發initialValue,也就是說get的時候如果沒拿到東西,則會觸發initialValue,
3.1.3、補充說明
- 通常,每個執行緒最多呼叫一次此方法,但是如果已經呼叫了
remove(),然后再次呼叫get()的話,則可以再次觸發initialValue, - 如果要重寫的話一般建議采取匿名內部類的方式重寫此方法,否則默認回傳的是null,
比如:
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); } }; // Java8的高逼格寫法 public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
3.1.4、原始碼
// 由子類提供實作, // protected的含義就是交給子類干的, protected T initialValue() { return null; }
3.2、get
3.2.1、什么意思
獲取當前執行緒下的ThreadLocal中的值,
3.2.2、原始碼
/** * 獲取當前執行緒下的entry里的value值, * 先獲取當前執行緒下的ThreadLocalMap, * 然后以當前ThreadLocal為key取出map中的value */ public T get() { // 獲取當前執行緒 Thread t = Thread.currentThread(); // 獲取當前執行緒對應的ThreadLocalMap物件, ThreadLocalMap map = getMap(t); // 若獲取到了,則獲取此ThreadLocalMap下的entry物件,若entry也獲取到了,那么直接獲取entry對應的value回傳即可, if (map != null) { // 獲取此ThreadLocalMap下的entry物件 ThreadLocalMap.Entry e = map.getEntry(this); // 若entry也獲取到了 if (e != null) { @SuppressWarnings("unchecked") // 直接獲取entry對應的value回傳, T result = (T)e.value; return result; } } // 若沒獲取到ThreadLocalMap或沒獲取到Entry,則設定初始值, // 知識點:我早就說了,初始值方法是延遲加載,只有在get才會用到,這下看到了吧,只有在這獲取沒獲取到才會初始化,下次就肯定有值了,所以只會執行一次!!! return setInitialValue(); }
3.3、set
3.3.1、什么意思
其實干的事和initialValue是一樣的,都是set值,只是呼叫時機不同,set是想用就用,api擺在這里,你想用就調一下set方法,很自由,
3.3.2、原始碼
/** * 設定當前執行緒的執行緒區域變數的值 * 實際上ThreadLocal的值是放入了當前執行緒的一個ThreadLocalMap實體中,所以只能在本執行緒中訪問, */ public void set(T value) { // 獲取當前執行緒 Thread t = Thread.currentThread(); // 獲取當前執行緒對應的ThreadLocalMap實體,注意這里是將t傳進去了,t是當前執行緒,就是說ThreadLocalMap是在執行緒里持有的參考, ThreadLocalMap map = getMap(t); // 若當前執行緒有對應的ThreadLocalMap實體,則將當前ThreadLocal物件作為key,value做為值存到ThreadLocalMap的entry里, if (map != null) map.set(this, value); else // 若當前執行緒沒有對應的ThreadLocalMap實體,則創建ThreadLocalMap,并將此執行緒與之系結 createMap(t, value); }
3.4、remove
3.4.1、什么意思
將當前執行緒下的ThreadLocal的值洗掉,目的是為了減少記憶體占用,主要目的是防止記憶體泄漏,記憶體泄漏問題下面會說,
3.4.2、原始碼
/** * 將當前執行緒區域變數的值洗掉,目的是為了減少記憶體占用,主要目的是防止記憶體泄漏,記憶體泄漏問題下面會說, */ public void remove() { // 獲取當前執行緒的ThreadLocalMap物件,并將其移除, ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) // 直接移除以當前ThreadLocal為key的value m.remove(this); }
4、ThreadLocalMap
為啥單獨拿出來說下,我就是想強調一點:這個東西是歸Thread類所有的,它的參考在Thread類里,這也證實了一個問題:ThreadLocalMap類內部為什么有Entry陣列,而不是Entry物件?
因為你業務代碼能new好多個ThreadLocal物件,各司其職,但是在一次請求里,也就是一個執行緒里,ThreadLocalMap是同一個,而不是多個,不管你new幾次ThreadLocal,ThreadLocalMap在一個執行緒里就一個,因為再說一次,ThreadLocalMap的參考是在Thread里的,所以它里面的Entry陣列存放的是一個執行緒里你new出來的多個ThreadLocal物件,
核心原始碼如下:
// 在你呼叫ThreadLocal.get()方法的時候就會呼叫這個方法,它的回傳是當前執行緒里的threadLocals的參考, // 這個參考指向的是ThreadLocal里的ThreadLocalMap物件 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } public class Thread implements Runnable { // ThreadLocal.ThreadLocalMap ThreadLocal.ThreadLocalMap threadLocals = null; }
四、完整原始碼
1、核心原始碼
// 本地執行緒,Thread:執行緒,Local:本地 public class ThreadLocal<T> { // 構造器 public ThreadLocal() {} // 初始值,用來初始化值用的,比如:ThreadLocal<Integer> count = new ThreadLocal<>(); // 你想Integer value = https://www.cnblogs.com/javazhiyin/p/count.get(); value++;這樣是報錯的,因為count現在還沒值,取出來的是個null,所以你需要先重寫此方法為value賦上初始值,本身方法是protected也代表就是為了子類重寫的, // 此方法是一個延遲呼叫方法,在執行緒第一次呼叫get的時候才執行,下面具體分析原始碼就知道了, protected T initialValue() {} // 創建ThreadLocalMap,ThreadLocal底層其實就是一個map來維護的, void createMap(Thread t, T firstValue) {} // 回傳該當前執行緒對應的執行緒區域變數值, public T get() {} // 獲取ThreadLocalMap ThreadLocalMap getMap(Thread t) {} // 設定當前執行緒的執行緒區域變數的值 public void set(T value) {} // 將當前執行緒區域變數的值洗掉,目的是為了減少記憶體占用,其實當執行緒結束后對應該執行緒的區域變數將自動被垃圾回收,所以無需我們呼叫remove,我們呼叫remove無非也就是加快記憶體回收速度, public void remove() {} // 設定初始值,呼叫initialValue private T setInitialValue() {} // 靜態內部類,一個map來維護的!!! static class ThreadLocalMap { // ThreadLocalMap的靜態內部類,繼承了弱參考,這正是不會造成記憶體泄漏根本原因 // Entry的key為ThreadLocal并且是弱參考,value是值 static class Entry extends WeakReference<ThreadLocal<?>> {} } }
2、set()
/** * 設定當前執行緒的執行緒區域變數的值 * 實際上ThreadLocal的值是放入了當前執行緒的一個ThreadLocalMap實體中,所以只能在本執行緒中訪問, */ public void set(T value) { // 獲取當前執行緒 Thread t = Thread.currentThread(); // 獲取當前執行緒對應的ThreadLocalMap實體 ThreadLocalMap map = getMap(t); // 若當前執行緒有對應的ThreadLocalMap實體,則將當前ThreadLocal物件作為key,value做為值存到ThreadLocalMap的entry里, if (map != null) map.set(this, value); else // 若當前執行緒沒有對應的ThreadLocalMap實體,則創建ThreadLocalMap,并將此執行緒與之系結 createMap(t, value); }
3、getMap()
// 在你呼叫ThreadLocal.get()方法的時候就會呼叫這個方法,它的回傳是當前執行緒里的threadLocals的參考, // 這個參考指向的是ThreadLocal里的ThreadLocalMap物件 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } public class Thread implements Runnable { // ThreadLocal.ThreadLocalMap ThreadLocal.ThreadLocalMap threadLocals = null; }
4、map.set()
// 不多BB,就和HashMap的set一個道理,只是賦值key,value, // 需要注意的是這里key是ThreadLocal物件,value是值 private void set(ThreadLocal<?> key, Object value) {}
5、createMap()
/** * 創建ThreadLocalMap物件, * t.threadLocals在上面的getMap中詳細介紹了,此處不BB, * 實體化ThreadLocalMap并且傳入兩個值,一個是當前ThreadLocal物件一個是value, */ void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } // ThreadLocalMap構造器, ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 重點看這里!!!!!! // new了一個ThreadLocalMap的內部類Entry,且將key和value傳入, // key是ThreadLocal物件, table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } /** * 到這里朋友們應該真相大白了,其實ThreadLocal就是內部維護一個ThreadLocalMap, * 而ThreadLocalMap內部又維護了一個Entry物件,Entry物件是key-value形式, * key是ThreadLocal物件,value是傳入的value * 所以我們對ThreadLocal的操作其實都是對內部的ThreadLocalMap.Entry的操作 * 所以保證了執行緒之前互不干擾, */
6、get()
/** * 獲取當前執行緒下的entry里的value值, * 先獲取當前執行緒下的ThreadLocalMap, * 然后以當前ThreadLocal為key取出map中的value */ public T get() { // 獲取當前執行緒 Thread t = Thread.currentThread(); // 獲取當前執行緒對應的ThreadLocalMap物件, ThreadLocalMap map = getMap(t); // 若獲取到了,則獲取此ThreadLocalMap下的entry物件,若entry也獲取到了,那么直接獲取entry對應的value回傳即可, if (map != null) { // 獲取此ThreadLocalMap下的entry物件 ThreadLocalMap.Entry e = map.getEntry(this); // 若entry也獲取到了 if (e != null) { @SuppressWarnings("unchecked") // 直接獲取entry對應的value回傳, T result = (T)e.value; return result; } } // 若沒獲取到ThreadLocalMap或沒獲取到Entry,則設定初始值, // 知識點:我早就說了,初始值方法是延遲加載,只有在get才會用到,這下看到了吧,只有在這獲取沒獲取到才會初始化,下次就肯定有值了,所以只會執行一次!!! return setInitialValue(); }
7、setInitialValue()
// 設定初始值 private T setInitialValue() { // 呼叫初始值方法,由子類提供, T value =https://www.cnblogs.com/javazhiyin/p/ initialValue(); // 獲取當前執行緒 Thread t = Thread.currentThread(); // 獲取map ThreadLocalMap map = getMap(t); // 獲取到了 if (map != null) // set map.set(this, value); else // 沒獲取到,創建map并賦值 createMap(t, value); // 回傳初始值, return value; }
8、initialValue()
// 由子類提供實作, // protected protected T initialValue() { return null; }
9、remove()
/** * 將當前執行緒區域變數的值洗掉,目的是為了減少記憶體占用, * 其實當執行緒結束后對應該執行緒的區域變數將自動被垃圾回收,所以無需我們呼叫remove,我們呼叫remove無非也就是加快記憶體回收速度, */ public void remove() { // 獲取當前執行緒的ThreadLocalMap物件,并將其移除, ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
10、小結
只要捋清楚如下幾個類的關系,ThreadLocal將變得so easy!
Thread、ThreadLocal、ThreadLocalMap、Entry
一句話總結就是:Thread維護了ThreadLocalMap,而ThreadLocalMap里維護了Entry,而Entry里存的是以ThreadLocal為key,傳入的值為value的鍵值對,
五、答疑(面試題)
1、和Synchronized的區別
問:他和執行緒同步機制(如:Synchronized)提供一樣的功能,這個很吊啊,
答:放屁!同步機制保證的是多執行緒同時操作共享變數并且能正確的輸出結果,ThreadLocal不行啊,他把共享變數變成執行緒私有了,每個執行緒都有獨立的一個變數,舉個通俗易懂的案例:網站計數器,你給變數count++的時候帶上synchronized即可解決,ThreadLocal的話做不到啊,他沒發統計,他只能說能統計每個執行緒登錄了多少次,
2、存盤在jvm的哪個區域
問:執行緒私有,那么就是說ThreadLocal的實體和他的值是放到堆疊上咯?
答:不是,還是在堆的,ThreadLocal物件也是物件,物件就在堆,只是JVM通過一些技巧將其可見性變成了執行緒可見,
3、真的只是當前執行緒可見嗎
問:真的只是當前執行緒可見嗎?
答:貌似不是,貌似通過InheritableThreadLocal類可以實作多個執行緒訪問ThreadLocal的值,但是我沒研究過,知道這碼事就行了,
4、會導致記憶體泄漏么
問:會導致記憶體泄漏么?
答:分析一下:
- 1、
ThreadLocalMap.Entry的key會記憶體泄漏嗎? - 2、
ThreadLocalMap.Entry的value會記憶體泄漏嗎?
先看下key-value的核心原始碼
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
先看繼承關系,發現是繼承了弱參考,而且key直接是交給了父類處理super(key),父類是個弱參考,所以key完全不存在記憶體泄漏問題,因為他不是強參考,它可以被GC回收的,
弱參考的特點:如果這個物件只被弱參考關聯,沒有任何強參考關聯,那么這個物件就可以被GC回收掉,弱參考不會阻止GC回收,這是jvm知識,
再看value,發現value是個強參考,但是想了下也沒問題的呀,因為執行緒終止了,我管你強參考還是弱參考,都會被GC掉的,因為參考鏈斷了(jvm用的可達性分析法,執行緒終止了,根節點就斷了,下面的都會被回收),
這么分析一點毛病都沒有,但是忘了一個主要的角色,那就是執行緒池,執行緒池的存在核心執行緒是不會銷毀的,只要創建出來他會反復利用,生命周期不會結束掉,但是key是弱參考會被GC回收掉,value強參考不會回收,所以形成了如下場面:
Thread->ThreadLocalMap->Entry(key為null)->value
由于value和Thread還存在鏈路關系,還是可達的,所以不會被回收,這樣越來越多的垃圾物件產生卻無法回收,早晨記憶體泄漏,時間久了必定OOM,
解決方案ThreadLocal已經為我們想好了,提供了remove()方法,這個方法是將value移出去的,所以用完后記得remove(),
5、為什么用Entry陣列而不是Entry物件
這個其實主要想考
ThreadLocalMap是在Thread里持有的參考,
問:ThreadLocalMap內部的table為什么是陣列而不是單個物件呢?
答:因為你業務代碼能new好多個ThreadLocal物件,各司其職,但是在一次請求里,也就是一個執行緒里,ThreadLocalMap是同一個,而不是多個,不管你new幾次ThreadLocal,ThreadLocalMap在一個執行緒里就一個,因為ThreadLocalMap的參考是在Thread里的,所以它里面的Entry陣列存放的是一個執行緒里你new出來的多個ThreadLocal物件,
6、你學習的開源框架哪些用到了ThreadLocal
Spring框架,
DateTimeContextHolder
RequestContextHolder
7、ThreadLocal里的物件一定是執行緒安全的嗎
未必,如果在每個執行緒中ThreadLocal.set()進去的東西本來就是多執行緒共享的同一個物件,比如static物件,那么多個執行緒的ThreadLocal.get()獲取的還是這個共享物件本身,還是有并發訪問執行緒不安全問題,
8、筆試題
問:下面這段程式會輸出什么?為什么?
public class TestThreadLocalNpe { private static ThreadLocal<Long> threadLocal = new ThreadLocal(); public static void set() { threadLocal.set(1L); } public static long get() { return threadLocal.get(); } public static void main(String[] args) throws InterruptedException { new Thread(() -> { set(); System.out.println(get()); }).start(); // 目的就是為了讓子執行緒先運行完 Thread.sleep(100); System.out.println(get()); } }
答:
1
Exception in thread "main" java.lang.NullPointerException
at com.chentongwei.study.thread.TestThreadLocalNpe.get(TestThreadLocalNpe.java:16)
at com.chentongwei.study.thread.TestThreadLocalNpe.main(TestThreadLocalNpe.java:26)
為什么?
為什么輸出個1,然后空指標了?
首先輸出1是沒任何問題的,其次主執行緒空指標是為什么?
如果你這里回答
1
1
那我恭喜你,你連ThreadLocal都不知道是啥,這明顯兩個執行緒,子執行緒和主執行緒,子執行緒設定1,主執行緒肯定拿不到啊,ThreadLocal和執行緒是嘻嘻相關的,這個不多費口舌,
說說為什么是空指標?
因為你get方法用的long而不是Long,那也應該回傳null啊,大哥,long是基本型別,默認值是0,沒有null這一說法,ThreadLocal里的泛型是Long,get卻是基本型別,這需要拆箱操作的,也就是會執行null.longValue()的操作,這絕逼空指標了,
看似一道Javase的基礎題目,實則隱藏了很多知識,
六、ThreadLocal工具類
package com.duoku.base.util; import com.google.common.collect.Maps; import org.springframework.core.NamedThreadLocal; import java.util.Map; /** * Description: * * @author TongWei.Chen 2019-09-09 18:35:30 */ public class ThreadLocalUtil { private static final ThreadLocal<Map<String, Object>> threadLocal = new NamedThreadLocal("xxx-threadlocal") { @Override protected Map<String, Object> initialValue() { return Maps.newHashMap(); } }; public static Map<String, Object> getThreadLocal(){ return threadLocal.get(); } public static <T> T get(String key) { Map map = threadLocal.get(); // todo:copy a new one return (T)map.get(key); } public static <T> T get(String key,T defaultValue) { Map map = threadLocal.get(); return (T)map.get(key) == null ? defaultValue : (T)map.get(key); } public static void set(String key, Object value) { Map map = threadLocal.get(); map.put(key, value); } public static void set(Map<String, Object> keyValueMap) { Map map = threadLocal.get(); map.putAll(keyValueMap); } public static void remove() { threadLocal.remove(); } }
另
瑣碎時間想看一些技術文章,可以去公眾號選單欄翻一翻我分類好的內容,應該對部分童鞋有幫助,同時看的程序中發現問題歡迎留言指出,不勝感謝~,另外,有想多了解哪些方面內容的可以留言(什么時候,哪篇文章下留言都行),附選單欄截圖(PS:很多人不知道公眾號選單欄是什么)

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/139413.html
標籤:Java
