哥,我還是不懂 ThreadLocal
大家好,我是風箏
前幾天群里有個弟弟說看 TheadLocal 有點懵,我就把之前寫的那篇給他扔過去了,結果他看完了跟我說:哥,我還是沒看懂啊!
什么,這意思就是我寫的那篇文章不行啊,看完了也看不懂,這怎么能行。于是我問他現在糾結在哪里了,啥地方不懂。經過一番溝通,我發現那篇文章確實寫得不太行,好多新手不理解的點都沒有點出來。
具體的一些容易讓人迷糊的點有以下幾個,雖然有一些問題看起來很傻,但是它們確實存在。
- ThreadLocal 存的值在不同線程間怎么傳遞?
- ThreadLocal 以什么形式存儲?
- ThreadLocal 可不可以放多個值?
- ThreadLocal 到底是存在哪?跟線程有什么關系?
咱們上來先看一段代碼精神精神,接下來再一一解釋上面的問題。這段代碼中聲明了兩個 ThreadLocal ,然后在線程0和線程1中分別賦值這兩個 ThreadLocal,第三個線程不賦值,在每個線程中打印這兩個 ThreadLocal 的值。
看一下應該輸出的值是多少。
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> threadLocal1 = ThreadLocal.withInitial(() -> "啥都沒干,初始值");
ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
Thread thread0 = new Thread() {
@Override
public void run() {
threadLocal1.set("我是threadLocal1 「Thread0」給我賦的值");
threadLocal2.set("我是threadLocal2 「Thread0」給我賦的值");
String name = "Thread0-";
System.out.println(name + "threadLocal1 = " + threadLocal1.get());
System.out.println(name + "threadLocal2 = " + threadLocal2.get());
}
};
thread0.start();
thread0.join();
System.out.println();
Thread thread1 = new Thread() {
@Override
public void run() {
threadLocal1.set("我是threadLocal1 「Thread1」給我賦的值");
threadLocal2.set("我是threadLocal2 「Thread1」給我賦的值");
String name = "Thread1-";
System.out.println(name + "threadLocal1 = " + threadLocal1.get());
System.out.println(name + "threadLocal = " + threadLocal2.get());
}
};
thread1.start();
thread1.join();
System.out.println();
Thread thread2 = new Thread() {
@Override
public void run() {
String name = "Thread2-";
System.out.println(name + "threadLocal1 = " + threadLocal1.get());
}
};
thread2.start();
}
下面是輸出的值,看看是不是和你理解的一致。
“
Thread0-threadLocal1 = 我是threadLocal1 「Thread0」給我賦的值 Thread0-threadLocal2 = 我是threadLocal2 「Thread0」給我賦的值
Thread1-threadLocal1 = 我是threadLocal1 「Thread1」給我賦的值 Thread1-threadLocal = 我是threadLocal2 「Thread1」給我賦的值
Thread2-threadLocal1 = 啥都沒干,初始值
如果和你想的輸出是一樣的,那你可能已經理解了 TheadLocal 了,如果不一致的話,那說明你還沒有掌握它。
問題1:ThreadLocal 存的值在不同線程間怎么傳遞?
我聽到這個問題有些詫異了,你真是一點兒都沒懂啊。ThreadLocal 當然不需要在進程間傳遞了,ThreadLocal 的初衷就是為了不在進程間傳遞值,而只是在當前線程的各個地方都能獲取到。
這就要說到 ThreadLocal 的定義和應用場景了。
ThreadLocal 定義以及使用場景
ThreadLocal允許每個線程獨立存儲和訪問線程本地變量。線程本地變量是指每個線程都有自己獨立的變量副本,互不干擾。這對于多線程編程來說非常有用,因為它允許在每個線程中存儲狀態或數據,而不需要擔心線程間的競爭條件。
我們進到 ThreadLocal 的源碼中,通過源碼注釋就可以看到很清晰的解釋:它是線程的局部變量,這些變量只能在這個線程內被讀寫,在其他線程內是無法訪問的。ThreadLocal 定義的通常是與線程關聯的私有靜態字段(例如,用戶ID或事務ID)。
變量有局部的還有全局的,局部變量沒什么好說的,一涉及到全局,那自然就會出現多線程的安全問題,要保證多線程安全訪問,不出現臟讀臟寫,那就要涉及到線程同步了。而 ThreadLocal 相當于提供了介于局部變量與全局變量中間的這樣一種線程內部的全局變量。
根據 ThreadLocal 的定義,我們就可以知道它的使用場景了。就是當我們只想在本身的線程內使用的變量,比如這個線程要存活一段時間,可以用 ThreadLocal 來實現,并且這些變量是和線程的生命周期密切相關的,線程結束,變量也就銷毀了。
舉幾個例子說明一下:
1、比如線程中處理一個非常復雜的業務,可能方法有很多,那么,使用 ThreadLocal 可以代替一些參數的顯式傳遞;
2、比如用來存儲用戶 Session。Session 的特性很適合 ThreadLocal ,因為 Session 之前當前會話周期內有效,會話結束便銷毀。我們先籠統的分析一次 web 請求的過程:
- 用戶在瀏覽器中訪問 web 頁面;
- 瀏覽器向服務器發起請求;
- 服務器上的服務處理程序(例如tomcat)接收請求,并開啟一個線程處理請求,期間會使用到 Session ;
- 最后服務器將請求結果返回給客戶端瀏覽器。
從這個簡單的訪問過程我們看到正好這個 Session 是在處理一個用戶會話過程中產生并使用的,如果單純的理解一個用戶的一次會話對應服務端一個獨立的處理線程,那用 ThreadLocal 在存儲 Session ,簡直是再合適不過了。但是例如 tomcat 這類的服務器軟件都是采用了線程池技術的,并不是嚴格意義上的一個會話對應一個線程。并不是說這種情況就不適合 ThreadLocal 了,而是要在每次請求進來時先清理掉之前的 Session ,一般可以用攔截器、過濾器來實現。
3、在一些多線程的情況下,如果用線程同步的方式,當并發比較高的時候會影響性能,可以改為 ThreadLocal 的方式,例如高性能序列化框架 Kyro 就要用 ThreadLocal 來保證高性能和線程安全;
4、還有像線程內上線文管理器、數據庫連接等可以用到 ThreadLocal;
使用方式
ThreadLocal 的使用非常簡單,最核心的操作就是四個:創建、創建并賦初始值、賦值、取值。
1、創建
ThreadLocal<String> mLocal = new ThreadLocal<>();
2、創建并賦初值。下面代碼表示創建了一個 String 類型的 ThreadLocal 并且重寫了 initialValue 方法,并返回初始字符串,之后調用 get() 方法獲取的值便是 initialValue 方法返回的值。
ThreadLocal<String> mLocal = new ThreadLocal<String>(){
@Override
protected String initialValue(){
return "init value";
}
};
System.out.println(mLocal.get());
3、設置值
mLocal.set("hello");
4、取值
mLocal.get()
實現原理
前面回答了第一個問題,后面的三個問題就涉及到 ThreadLocal 的原理了。
首先 ThreadLocal 是一個泛型類,保證可以接受任何類型的對象,所以你可以在 ThreadLocal 中放基本類型,比如字符串、整型等,也可以放自定義的實體對象,還可以放 List、Set、Map 等都沒有問題。
圖片
先來理清楚 ThreadLocal 對象的結構與線程的關系,我解釋一下上圖的意思。
- 在 Thread 類中有一個屬性叫做 threadLocals,這個屬性的類型是 ThreadLocal.ThreadLocalMap 類型;
- ThreadLocal 就是我們會直接用到的 ThreadLocal 對象;
- ThreadLocal 有個內部類 是 ThreadLocalMap,就是 Thread 類中的的 threadLocals 對象的類型;
- ThreadLocalMap 通過名稱可以看出這是一個 Map 結構,如果你看過 HashMap 的實現,就會發現它是個簡易版的 HashMap;
- ThreadLocalMap 中真正存儲數據的是一個 Entry 數組;
- Entry 又是ThreadLocalMap的一個靜態內部類, 它繼承 WeakReference 弱引用,暫且理解為是一個 key-value 鍵值對;其中涉及的重要對象大概就是上面這些,了解這些基礎后,能幫我們更清楚的理解原理。
看上去可能有點亂,最簡單的就是從 set 方法入手看一看。下面是 set 方法代碼
public void set(T value) {
Thread t = Thread.currentThread(); // 獲取當前線程
ThreadLocalMap map = getMap(t); // 獲取當前線程維護的 threadLocals
if (map != null)
map.set(this, value); // 如果 map 不為空,直接添加
else
createMap(t, value); //如果 map 為空,先初始化,再添加
}
調用 set 方法
ThreadLocal<String> mLocal = new ThreadLocal<>();
mLocal.set("hello");
調用 ThreadLocal 的 set 方法時,首先獲取到了當前線程。
Thread t = Thread.currentThread();
然后獲取當前線程維護的 ThreadLocalMap 對象。通過 getMap() 方法,t 就是當前線程,直接返回當前線程中的 threadLocals 屬性。
ThreadLocalMap map = getMap(t); //獲取 ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
如果 map 不為null,說明之前設置過 ThreadLocal 了,那就調用ThreadLocalMap 的set 方法。
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();
}
int i = key.threadLocalHashCode & (len-1);
計算索引,tab[i]就是要存儲的位置,后面 for 中的部分就是處理哈希沖突和更新已有值,先不用管這些細節,之后將 new Entry(key, value)放到 tab[i]的位置,也就是放到 Entry 數組中了。
這里面 new Entry中的參數 key 和 value 很關鍵。返回去看 ThreadLocalMap.set 方法調用時候的傳參。
map.set(this, value);
key 是什么呢?key 這里傳的是 this,this 是誰呢,就是 ThreadLocal 本身,它本身被當做 key 了。value 是什么呢?value 就是調用 ThreadLocal.set(value)時傳過來的泛型的值,是我們調用方自己設置的。
后面還有如果 ThreadLocalMap 實例不存在的話,則要初始化并賦初值的過程,這部分也不是理解 ThreadLocal 的重點,就不具體講了,看代碼都能理解。
所以后面那三個問題也就解決了。
現在再回過頭去看最開始給的那段代碼。
threadLocal1 和 threadLocal2 的聲明是在 main 方法中的,也就是在主線中聲明的,三個子線程都可以看到的。
而且線程0和線程1都用了兩個 ThreadLocal,所以說,一個線程可以用多個 ThreadLocal,因為最終存儲實際上是個 Map,多少個都沒關系。
線程 0 和線程1都對threadLocal1 和 threadLocal2重新設置值了,然后通過get方法得到的也是本線程設置的值。線程2沒有對 threadLocal1 賦值,所以在調用get方法后,得到的是threadLocal1最開始設置的初始值,并不是線程0或線程2設置的值。也印證了線程之間是不會互相影響的(當然,我們通過上面的分析已經了解這個原理了)。
內存泄漏問題
實際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,弱引用的特點是,如果這個對象只存在弱引用,那么在下一次垃圾回收的時候必然會被清理掉。
所以如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap 中使用這個 ThreadLocal 的 key 也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現 key 為 null 的 value。
ThreadLocalMap 實現中已經考慮了這種情況,在調用 set()、get()、remove() 方法的時候,會清理掉 key 為 null 的記錄。如果說會出現內存泄漏,那只有在出現了 key 為 null 的記錄后,沒有手動調用 remove() 方法,并且之后也不再調用 get()、set()、remove() 方法的情況下。
這回,理解了嗎?