ThreadLocal奪命11連問
前言
前一段時間,有同事使用ThreadLocal
踩坑了,正好引起了我的興趣。
所以近期,我抽空把ThreadLocal的源碼再研究了一下,越看越有意思,發現裡面的東西還真不少。
我把精華濃縮了一下,彙集成了下面11個問題,看看你能頂住第幾個?
1. 為什麼要用ThreadLocal?
並發編程是一項非常重要的技術,它讓我們的程式變得更加高效。
但在並發的場景中,如果有多個執行緒同時修改公共變數,可能會出現執行緒安全問題,即該變數最終結果可能出現異常。
為了解決執行緒安全問題,JDK
出現了很多技術手段,比如:使用synchronized
或Lock
,給訪問公共資源的程式碼上鎖,保證了程式碼的原子性
。
但在高並發的場景中,如果多個執行緒同時競爭一把鎖,這時會存在大量的鎖等待,可能會浪費很多時間,讓系統的響應時間一下子變慢。
因此,JDK
還提供了另外一種用空間換時間的新思路:ThreadLocal
。
它的核心思想是:共享變數在每個執行緒
都有一個副本
,每個執行緒操作的都是自己的副本,對另外的執行緒沒有影響。
例如:
@Service
public class ThreadLocalService {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public void add() {
threadLocal.set(1);
doSamething();
Integer integer = threadLocal.get();
}
}
2. ThreadLocal的原理是什麼?
為了搞清楚ThreadLocal的底層實現原理,我們不得不扒一下源碼。
ThreadLocal
的內部有一個靜態的內部類叫:ThreadLocalMap
。
public class ThreadLocal<T> {
...
public T get() {
//獲取當前執行緒
Thread t = Thread.currentThread();
//獲取當前執行緒的成員變數ThreadLocalMap對象
ThreadLocalMap map = getMap(t);
if (map != null) {
//根據threadLocal對象從map中獲取Entry對象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//獲取保存的數據
T result = (T)e.value;
return result;
}
}
//初始化數據
return setInitialValue();
}
private T setInitialValue() {
//獲取要初始化的數據
T value = initialValue();
//獲取當前執行緒
Thread t = Thread.currentThread();
//獲取當前執行緒的成員變數ThreadLocalMap對象
ThreadLocalMap map = getMap(t);
//如果map不為空
if (map != null)
//將初始值設置到map中,key是this,即threadLocal對象,value是初始值
map.set(this, value);
else
//如果map為空,則需要創建新的map對象
createMap(t, value);
return value;
}
public void set(T value) {
//獲取當前執行緒
Thread t = Thread.currentThread();
//獲取當前執行緒的成員變數ThreadLocalMap對象
ThreadLocalMap map = getMap(t);
//如果map不為空
if (map != null)
//將值設置到map中,key是this,即threadLocal對象,value是傳入的value值
map.set(this, value);
else
//如果map為空,則需要創建新的map對象
createMap(t, value);
}
static class ThreadLocalMap {
...
}
...
}
ThreadLocal
的get
方法、set
方法和setInitialValue
方法,其實最終操作的都是ThreadLocalMap
類中的數據。
其中ThreadLocalMap
類的內部如下:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
private Entry[] table;
...
}
ThreadLocalMap
裡面包含一個靜態的內部類Entry
,該類繼承於WeakReference
類,說明Entry
是一個弱引用。
ThreadLocalMap
內部還包含了一個Entry
數組,其中:Entry
= ThreadLocal
+ value
。
而ThreadLocalMap
被定義成了Thread
類的成員變數。
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
}
下面用一張圖從宏觀上,認識一下ThreadLocal的整體結構:
從上圖中看出,在每個Thread
類中,都有一個ThreadLocalMap
的成員變數,該變數包含了一個Entry數組
,該數組真正保存了ThreadLocal類set的數據。
Entry
是由threadLocal和value組成,其中threadLocal對象是弱引用,在GC
的時候,會被自動回收。而value就是ThreadLocal類set的數據。
下面用一張圖總結一下引用關係:
上圖中除了Entry的key對ThreadLocal對象是弱引用
,其他的引用都是強引用
。
需要特別說明的是,上圖中ThreadLocal對象我畫到了堆上,其實在實際的業務場景中不一定在堆上。因為如果ThreadLocal被定義成了static的,ThreadLocal的對象是類共用的,可能出現在方法區。
3. 為什麼用ThreadLocal做key?
不知道你有沒有思考過這樣一個問題:ThreadLocalMap
為什麼要用ThreadLocal
做key,而不是用Thread
做key?
如果在你的應用中,一個執行緒中只使用了一個ThreadLocal
對象,那麼使用Thread
做key也未嘗不可。
@Service
public class ThreadLocalService {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
}
但實際情況中,你的應用,一個執行緒中很有可能不只使用了一個ThreadLocal對象。這時使用Thread
做key不就出有問題?
@Service
public class ThreadLocalService {
private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
private static final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
}
假如使用Thread
做key時,你的程式碼中定義了3個ThreadLocal對象,那麼,通過Thread對象,它怎麼知道要獲取哪個ThreadLocal對象呢?
如下圖所示:
因此,不能使用Thread
做key,而應該改成用ThreadLocal
對象做key,這樣才能通過具體ThreadLocal對象的get
方法,輕鬆獲取到你想要的ThreadLocal對象。
如下圖所示:
4. Entry的key為什麼設計成弱引用?
前面說過,Entry的key,傳入的是ThreadLocal對象,使用了WeakReference
對象,即被設計成了弱引用。
那麼,為什麼要這樣設計呢?
假如key對ThreadLocal對象的弱引用,改為強引用。
我們都知道ThreadLocal變數對ThreadLocal對象是有強引用存在的。
即使ThreadLocal變數生命周期完了,設置成null了,但由於key對ThreadLocal還是強引用。
此時,如果執行該程式碼的執行緒
使用了執行緒池
,一直長期存在,不會被銷毀。
就會存在這樣的強引用鏈
:Thread變數 -> Thread對象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal對象。
那麼,ThreadLocal對象和ThreadLocalMap都將不會被GC
回收,於是產生了記憶體泄露
問題。
為了解決這個問題,JDK的開發者們把Entry的key設計成了弱引用
。
弱引用
的對象,在GC做垃圾清理的時候,就會被自動回收了。
如果key是弱引用,當ThreadLocal變數指向null之後,在GC做垃圾清理的時候,key會被自動回收,其值也被設置成null。
如下圖所示:
接下來,最關鍵的地方來了。
由於當前的ThreadLocal變數已經被指向null
了,但如果直接調用它的get
、set
或remove
方法,很顯然會出現空指針異常
。因為它的生命已經結束了,再調用它的方法也沒啥意義。
此時,如果系統中還定義了另外一個ThreadLocal變數b,調用了它的get
、set
或remove
,三個方法中的任何一個方法,都會自動觸發清理機制,將key為null的value值清空。
如果key和value都是null,那麼Entry對象會被GC回收。如果所有的Entry對象都被回收了,ThreadLocalMap也會被回收了。
這樣就能最大程度的解決記憶體泄露
問題。
需要特別注意的地方是:
- key為null的條件是,ThreadLocal變數指向
null
,並且key是弱引用。如果ThreadLocal變數沒有斷開對ThreadLocal的強引用,即ThreadLocal變數沒有指向null,GC就貿然的把弱引用的key回收了,不就會影響正常用戶的使用? - 如果當前ThreadLocal變數指向
null
了,並且key也為null了,但如果沒有其他ThreadLocal變數觸發get
、set
或remove
方法,也會造成記憶體泄露。
下面看看弱引用的例子:
public static void main(String[] args) {
WeakReference<Object> weakReference0 = new WeakReference<>(new Object());
System.out.println(weakReference0.get());
System.gc();
System.out.println(weakReference0.get());
}
列印結果:
java.lang.Object@1ef7fe8e
null
傳入WeakReference構造方法的是直接new處理的對象,沒有其他引用,在調用gc方法後,弱引用對象會被自動回收。
但如果出現下面這種情況:
public static void main(String[] args) {
Object object = new Object();
WeakReference<Object> weakReference1 = new WeakReference<>(object);
System.out.println(weakReference1.get());
System.gc();
System.out.println(weakReference1.get());
}
執行結果:
java.lang.Object@1ef7fe8e
java.lang.Object@1ef7fe8e
先定義了一個強引用object對象,在WeakReference構造方法中將object對象的引用作為參數傳入。這時,調用gc後,弱引用對象不會被自動回收。
我們的Entry對象中的key不就是第二種情況嗎?在Entry構造方法中傳入的是ThreadLocal對象的引用。
如果將object強引用設置為null:
public static void main(String[] args) {
Object object = new Object();
WeakReference<Object> weakReference1 = new WeakReference<>(object);
System.out.println(weakReference1.get());
System.gc();
System.out.println(weakReference1.get());
object=null;
System.gc();
System.out.println(weakReference1.get());
}
執行結果:
java.lang.Object@6f496d9f
java.lang.Object@6f496d9f
null
第二次gc之後,弱引用能夠被正常回收。
由此可見,如果強引用和弱引用同時關聯一個對象,那麼這個對象是不會被GC回收。也就是說這種情況下Entry的key,一直都不會為null,除非強引用主動斷開關聯。
此外,你可能還會問這樣一個問題:Entry的value為什麼不設計成弱引用?
答:Entry的value如果只是被Entry引用,有可能沒被業務系統中的其他地方引用。如果將value改成了弱引用,被GC貿然回收了(數據突然沒了),可能會導致業務系統出現異常。
而相比之下,Entry的key,管理的地方就非常明確了。
這就是Entry的key被設計成弱引用,而value沒被設計成弱引用的原因。
5. ThreadLocal真的會導致記憶體泄露?
通過上面的Entry對象中的key設置成弱引用,並且使用get
、set
或remove
方法清理key為null的value值,就能徹底解決記憶體泄露問題?
答案是否定的。
如下圖所示:
假如ThreadLocalMap中存在很多key為null的Entry,但後面的程式,一直都沒有調用過有效的ThreadLocal的get
、set
或remove
方法。
那麼,Entry的value值一直都沒被清空。
所以會存在這樣一條強引用鏈
:Thread變數 -> Thread對象 -> ThreadLocalMap -> Entry -> value -> Object。
其結果就是:Entry和ThreadLocalMap將會長期存在下去,會導致記憶體泄露
。
6. 如何解決記憶體泄露問題?
前面說過的ThreadLocal還是會導致記憶體泄露的問題,我們有沒有解決辦法呢?
答:有辦法,調用ThreadLocal對象的remove
方法。
不是在一開始就調用remove方法,而是在使用完ThreadLocal對象之後。列如:
先創建一個CurrentUser類,其中包含了ThreadLocal的邏輯。
public class CurrentUser {
private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();
public static void set(UserInfo userInfo) {
THREA_LOCAL.set(userInfo);
}
public static UserInfo get() {
THREA_LOCAL.get();
}
public static void remove() {
THREA_LOCAL.remove();
}
}
然後在業務程式碼中調用相關方法:
public void doSamething(UserDto userDto) {
UserInfo userInfo = convert(userDto);
try{
CurrentUser.set(userInfo);
...
//業務程式碼
UserInfo userInfo = CurrentUser.get();
...
} finally {
CurrentUser.remove();
}
}
需要我們特別注意的地方是:一定要在finally
程式碼塊中,調用remove
方法清理沒用的數據。如果業務程式碼出現異常,也能及時清理沒用的數據。
remove
方法中會把Entry中的key和value都設置成null,這樣就能被GC及時回收,無需觸發額外的清理機制,所以它能解決記憶體泄露問題。
7. ThreadLocal是如何定位數據的?
前面說過ThreadLocalMap對象底層是用Entry數組保存數據的。
那麼問題來了,ThreadLocal是如何定位Entry數組數據的?
在ThreadLocal的get、set、remove方法中都有這樣一行程式碼:
int i = key.threadLocalHashCode & (len-1);
通過key的hashCode值,與
數組的長度減1。其中key就是ThreadLocal對象,與
數組的長度減1,相當於除以數組的長度減1,然後取模
。
這是一種hash演算法。
接下來給大家舉個例子:假設len=16,key.threadLocalHashCode=31,
於是: int i = 31 & 15 = 15
相當於:int i = 31 % 16 = 15
計算的結果是一樣的,但是使用與運算
效率跟高一些。
為什麼與運算效率更高?
答:因為ThreadLocal的初始大小是16
,每次都是按2
倍擴容,數組的大小其實一直都是2的n次方。這種數據有個規律就是高位是0,低位都是1。在做與運算時,可以不用考慮高位,因為與運算的結果必定是0。只需考慮低位的與運算,所以效率更高。
如果使用hash演算法定位具體位置的話,就可能會出現hash衝突
的情況,即兩個不同的hashCode取模後的值相同。
ThreadLocal是如何解決hash衝突的呢?
我們看看getEntry
是怎麼做的:
private Entry getEntry(ThreadLocal<?> key) {
//通過hash演算法獲取下標值
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//如果下標位置上的key正好是我們所需要尋找的key
if (e != null && e.get() == key)
//說明找到數據了,直接返回
return e;
else
//說明出現hash衝突了,繼續往後找
return getEntryAfterMiss(key, i, e);
}
再看看getEntryAfterMiss
方法:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//判斷Entry對象如果不為空,則一直循環
while (e != null) {
ThreadLocal<?> k = e.get();
//如果當前Entry的key正好是我們所需要尋找的key
if (k == key)
//說明這次真的找到數據了
return e;
if (k == null)
//如果key為空,則清理臟數據
expungeStaleEntry(i);
else
//如果還是沒找到數據,則繼續往後找
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
關鍵看看nextIndex
方法:
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
當通過hash演算法計算出的下標小於數組大小,則將下標值加1。否則,即下標大於等於數組大小,下標變成0了。下標變成0之後,則循環一次,下標又變成1。。。
尋找的大致過程如下圖所示:
如果找到最後一個,還是沒有找到,則再從頭開始找。
不知道你有沒有發現,它構成了一個:環形
。
ThreadLocal從數組中找數據的過程大致是這樣的:
- 通過key的hashCode取余計算出一個下標。
- 通過下標,在數組中定位具體Entry,如果key正好是我們所需要的key,說明找到了,則直接返回數據。
- 如果第2步沒有找到我們想要的數據,則從數組的下標位置,繼續往後面找。
- 如果第3步中找key的正好是我們所需要的key,說明找到了,則直接返回數據。
- 如果還是沒有找到數據,再繼續往後面找。如果找到最後一個位置,還是沒有找到數據,則再從頭,即下標為0的位置,繼續從前往後找數據。
- 直到找到第一個Entry為空為止。
8. ThreadLocal是如何擴容的?
從上面得知,ThreadLocal的初始大小是16
。那麼問題來了,ThreadLocal是如何擴容的?
在set
方法中會調用rehash
方法:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
注意一下,其中有個判斷條件是:sz(之前的size+1)如果大於或等於threshold的話,則調用rehash方法。
threshold默認是0,在創建ThreadLocalMap時,調用它的構造方法:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
調用setThreshold方法給threshold設置一個值,而這個值INITIAL_CAPACITY是默認的大小16。
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
也就是第一次設置的threshold = 16 * 2 / 3, 取整後的值是:10。
換句話說當sz大於等於10時,就可以考慮擴容了。
rehash程式碼如下:
private void rehash() {
//先嘗試回收一次key為null的值,騰出一些空間
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
在真正擴容之前,先嘗試回收一次key為null的值,騰出一些空間。
如果回收之後的size大於等於threshold的3/4時,才需要真正的擴容。
計算公式如下:
16 * 2 * 4 / 3 * 4 - 16 * 2 / 3 * 4 = 8
也就是說添加數據後,新的size大於等於老size的1/2
時,才需要擴容。
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//按2倍的大小擴容
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
resize中每次都是按2倍的大小擴容。
擴容的過程如下圖所示:
擴容的關鍵步驟如下:
- 老size + 1 = 新size
- 如果新size大於等於老size的2/3時,需要考慮擴容。
- 擴容前先嘗試回收一次key為null的值,騰出一些空間。
- 如果回收之後發現size還是大於等於老size的1/2時,才需要真正的擴容。
- 每次都是按2倍的大小擴容。
9. 父子執行緒如何共享數據?
前面介紹的ThreadLocal都是在一個執行緒
中保存和獲取數據的。
但在實際工作中,有可能是在父子執行緒中共享數據的。即在父執行緒中往ThreadLocal設置了值,在子執行緒中能夠獲取到。
例如:
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.set(6);
System.out.println("父執行緒獲取數據:" + threadLocal.get());
new Thread(() -> {
System.out.println("子執行緒獲取數據:" + threadLocal.get());
}).start();
}
}
執行結果:
父執行緒獲取數據:6
子執行緒獲取數據:null
你會發現,在這種情況下使用ThreadLocal是行不通的。main方法是在主執行緒中執行的,相當於父執行緒。在main方法中開啟了另外一個執行緒,相當於子執行緒。
顯然通過ThreadLocal,無法在父子執行緒中共享數據。
那麼,該怎麼辦呢?
答:使用InheritableThreadLocal
,它是JDK自帶的類,繼承了ThreadLocal類。
修改程式碼之後:
public class ThreadLocalTest {
public static void main(String[] args) {
InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
threadLocal.set(6);
System.out.println("父執行緒獲取數據:" + threadLocal.get());
new Thread(() -> {
System.out.println("子執行緒獲取數據:" + threadLocal.get());
}).start();
}
}
執行結果:
父執行緒獲取數據:6
子執行緒獲取數據:6
果然,在換成InheritableThreadLocal之後,在子執行緒中能夠正常獲取父執行緒中設置的值。
其實,在Thread類中除了成員變數threadLocals之外,還有另一個成員變數:inheritableThreadLocals。
Thread類的部分程式碼如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
最關鍵的一點是,在它的init
方法中會將父執行緒中往ThreadLocal設置的值,拷貝一份到子執行緒中。
感興趣的小夥伴,可以找我私聊。或者看看我後面的文章,後面還會有專欄。
10. 執行緒池中如何共享數據?
在真實的業務場景中,一般很少用單獨的執行緒
,絕大多數,都是用的執行緒池
。
那麼,在執行緒池中如何共享ThreadLocal對象生成的數據呢?
因為涉及到不同的執行緒,如果直接使用ThreadLocal,顯然是不合適的。
我們應該使用InheritableThreadLocal,具體程式碼如下:
private static void fun1() {
InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
threadLocal.set(6);
System.out.println("父執行緒獲取數據:" + threadLocal.get());
ExecutorService executorService = Executors.newSingleThreadExecutor();
threadLocal.set(6);
executorService.submit(() -> {
System.out.println("第一次從執行緒池中獲取數據:" + threadLocal.get());
});
threadLocal.set(7);
executorService.submit(() -> {
System.out.println("第二次從執行緒池中獲取數據:" + threadLocal.get());
});
}
執行結果:
父執行緒獲取數據:6
第一次從執行緒池中獲取數據:6
第二次從執行緒池中獲取數據:6
由於這個例子中使用了單例執行緒池,固定執行緒數是1。
第一次submit任務的時候,該執行緒池會自動創建一個執行緒。因為使用了InheritableThreadLocal,所以創建執行緒時,會調用它的init方法,將父執行緒中的inheritableThreadLocals數據複製到子執行緒中。所以我們看到,在主執行緒中將數據設置成6,第一次從執行緒池中獲取了正確的數據6。
之後,在主執行緒中又將數據改成7,但在第二次從執行緒池中獲取數據卻依然是6。
因為第二次submit任務的時候,執行緒池中已經有一個執行緒了,就直接拿過來複用,不會再重新創建執行緒了。所以不會再調用執行緒的init方法,所以第二次其實沒有獲取到最新的數據7,還是獲取的老數據6。
那麼,這該怎麼辦呢?
答:使用TransmittableThreadLocal
,它並非JDK自帶的類,而是阿里巴巴開源jar包中的類。
可以通過如下pom文件引入該jar包:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.11.0</version>
<scope>compile</scope>
</dependency>
程式碼調整如下:
private static void fun2() throws Exception {
TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
threadLocal.set(6);
System.out.println("父執行緒獲取數據:" + threadLocal.get());
ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
threadLocal.set(6);
ttlExecutorService.submit(() -> {
System.out.println("第一次從執行緒池中獲取數據:" + threadLocal.get());
});
threadLocal.set(7);
ttlExecutorService.submit(() -> {
System.out.println("第二次從執行緒池中獲取數據:" + threadLocal.get());
});
}
執行結果:
父執行緒獲取數據:6
第一次從執行緒池中獲取數據:6
第二次從執行緒池中獲取數據:7
我們看到,使用了TransmittableThreadLocal之後,第二次從執行緒中也能正確獲取最新的數據7了。
nice。
如果你仔細觀察這個例子,你可能會發現,程式碼中除了使用TransmittableThreadLocal
類之外,還使用了TtlExecutors.getTtlExecutorService
方法,去創建ExecutorService
對象。
這是非常重要的地方,如果沒有這一步,TransmittableThreadLocal
在執行緒池中共享數據將不會起作用。
創建ExecutorService
對象,底層的submit方法會TtlRunnable
或TtlCallable
對象。
以TtlRunnable類為例,它實現了Runnable
介面,同時還實現了它的run方法:
public void run() {
Map<TransmittableThreadLocal<?>, Object> copied = (Map)this.copiedRef.get();
if (copied != null && (!this.releaseTtlValueReferenceAfterRun || this.copiedRef.compareAndSet(copied, (Object)null))) {
Map backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
try {
this.runnable.run();
} finally {
TransmittableThreadLocal.restoreBackup(backup);
}
} else {
throw new IllegalStateException("TTL value reference is released after run!");
}
}
這段程式碼的主要邏輯如下:
- 把當時的ThreadLocal做個備份,然後將父類的ThreadLocal拷貝過來。
- 執行真正的run方法,可以獲取到父類最新的ThreadLocal數據。
- 從備份的數據中,恢復當時的ThreadLocal數據。
11. ThreadLocal有哪些用途?
最後,一起聊聊ThreadLocal有哪些用途?
老實說,使用ThreadLocal的場景挺多的。
下面列舉幾個常見的場景:
- 在spring事務中,保證一個執行緒下,一個事務的多個操作拿到的是一個Connection。
- 在hiberate中管理session。
- 在JDK8之前,為了解決SimpleDateFormat的執行緒安全問題。
- 獲取當前登錄用戶上下文。
- 臨時保存許可權數據。
- 使用MDC保存日誌資訊。
等等,還有很多業務場景,這裡就不一一列舉了。
由於篇幅有限,今天的內容先分享到這裡。希望你看了這篇文章,會有所收穫。
接下來留幾個問題給大家思考一下:
- ThreadLocal變數為什麼建議要定義成static的?
- Entry數組為什麼要通過hash演算法計算下標,即直線定址法,而不直接使用下標值?
- 強引用和弱引用有什麼區別?
- Entry數組大小,為什麼是2的N次方?
- 使用InheritableThreadLocal時,如果父執行緒中重新set值,在子執行緒中能夠正確的獲取修改後的新值嗎?
敬請期待我的下一篇文章,謝謝。
最後說一句(求關注,別白嫖我)
如果這篇文章對您有所幫助,或者有所啟發的話,幫忙掃描下發二維碼關注一下,您的支援是我堅持寫作最大的動力。
求一鍵三連:點贊、轉發、在看。
關注公眾號:【蘇三說技術】,在公眾號中回復:面試、程式碼神器、開發手冊、時間管理有超贊的粉絲福利,另外回復:加群,可以跟很多BAT大廠的前輩交流和學習。