注:此文源碼摘自 sun jdk 1.8
ThreadLocal 是什麼
打開 ThreadLocal 的源碼我們可以看到如下的註釋:
大致翻譯如下:
該類提供了線程局部 (thread-local) 變量。這些變量不同於它們的普通對應物,因爲訪問某個變量(通過其 get 或 set 方法)的每個線程都有自己的局部變量,它獨立於變量的初始化副本。ThreadLocal 實例通常是類中的 private static 字段,它們希望將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。
例如,以下類生成對每個線程唯一的局部標識符。 線程 ID 是在第一次調用 UniqueThreadIdGenerator.getCurrentThreadId() 時分配的,在後續調用中不會更改。
import java.util.concurrent.atomic.AtomicInteger;
public class UniqueThreadIdGenerator {
private static final AtomicInteger uniqueId = new AtomicInteger(0);
private static final ThreadLocal < Integer > uniqueNum =
new ThreadLocal < Integer > () {
@Override protected Integer initialValue() {
return uniqueId.getAndIncrement();
}
};
public static int getCurrentThreadId() {
return uniqueId.get();
}
} // UniqueThreadIdGenerator
每個線程都保持對其線程局部變量副本的隱式引用,只要線程是活動的並且 ThreadLocal 實例是可訪問的;在線程消失之後,其線程局部實例的所有副本都會被垃圾回收(除非存在對這些副本的其他引用)。
簡單而言,ThreadLocal
是一個線程自身內部的數據存儲類,其它線程是無法訪問該線程的數據的。這樣說起來還是挺抽象的,我們下面來介紹一個例子。
ThreadLocal 的使用
代碼如下:
public class Test {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "default";
}
};
public static void main(String[] args) {
threadLocal.set("thread#0");
System.out.println("thread#0 " + threadLocal.get());
new Thread("thread#1") {
@Override
public void run() {
System.out.println(currentThread().getName() + " " + threadLocal.get());
}
}.start();
new Thread("thread#2") {
@Override
public void run() {
threadLocal.set("thread#2");
System.out.println(currentThread().getName() + " " + threadLocal.get());
}
}.start();
}
}
我們首先創建一個 ThreadLocal<String>
對象,並重寫它的 initialValue()
方法,這樣它默認情況下就是返回一個字符串爲 default
的字段。然後我們再 main()
方法裏面創建兩個線程,線程 thread#1
和 thread#2
,當然,加上它本身,一共是三個線程,我們分別在主線程調用 set("thread#0")
;thread#1
線程不調用 set()
方法;thread#2
調用 set("thread#2")
方法。我們發現,輸出結果如下:
對於主線程,由於我們調用了 set("thread#0")
方法,所以 threadLocal.get()
的值就是字符串 thread#0
;對於 thread#1
線程沒有調用 set()
方法,故 threadLocal.get()
返回的就是默認值 default
字符串;對於 thread#2
線程,我們調用了 set("thread#2")
方法,故 threadLoca.get()
返回的就是字符串 thread#2
。
所以我們可以發現,即使是同一個 ThreadLocal
對象,我們的 set()
或 get()
方法都是僅對當前線程可見的,各個線程之間不可見不可相互影響。
ThreadLocal 源碼解析
要想搞清楚 ThreadLocal
爲什麼會有這樣的特性,我們其實只需要搞清楚我們上面所使用到的 set()
和 get()
方法即可。set()
方法源碼如下:
第一行代碼不解釋了。我們看到第二行代碼可以獲取到一個 ThreadLocalMap,我們不妨戳進 getMap()
方法裏面看一下,ThreadLocalMap
是如何和 Thread
參數關聯在一起的,源碼如下:
我們再戳進 Thread
類看一下 ——
這下清楚了,先獲取當前的線程對象,再獲取這個對象中的 ThreadLocalMap
對象,並且調用它的 set()
方法將當前的 ThreadLocal
對象和傳入的 value 值存入。那麼 ThreadLocalMap
又是個什麼呢?它的 set()
方法肯定是個關鍵點,又是怎樣的呢?我們再來看看 ——
ThreadLocalMap
其實就是 ThreadLocal
的一個內部類
到這裏就一目瞭然了,ThreadLocalMap
中維護了一個 Entry[]
(Entry
是一個泛型類,其構造函數需要傳入兩個參數,一個是 ThreadLocal
對象,作爲 Reference,另一個是 Object 對象,作爲 value),然後我們就只需要將當前的 ThreadLocal
對象和傳入的值放入這個數組的某個位置就可以了。
接下來我們再來看看 get()
方法,其源碼如下:
這個源碼很簡單了,就是獲取到當前線程對象的所持有的 ThreadLocalMap
對象,傳入當前的 ThreadLocal
對象由此獲得到對應的 ThreadLocalMap.Entry
對象,再取出它的 value 即可。
到此所有的流程已經走了一遍了,我們再來理一遍思路:首先創建一個全局共享的 ThreadLocal
對象,因爲 ThreadLocal
不應該是依賴某一個具體的線程的,然後假設我們使用了三個線程 A、B、C 來對該 ThreadLocal
對象進行操作,線程 A 先調用 ThreadLocal
對象的 set()
方法,那麼線程 A 中的 ThreadLocalMap
裏面的 Entry
數組就會在某個位置保存這個 ThreadLocal
對象和 set()
進來的值;然後線程 B 調用 ThreadLocal
對象的 set()
方法,那麼線程 B 中的 ThreadLocalMap
裏面的 Entry
數組就會在某個位置保存這個 ThreadLocal
對象和 set()
進來的值;然後線程 C 調用 ThreadLocal
對象的 set()
方法,那麼線程 C 中的 ThreadLocalMap
裏面的 Entry
數組就會在某個位置保存這個 ThreadLocal
對象和 set()
進來的值。每個線程所持有的 ThreadLocalMap
對象是不同的,故其 ThreadLocalMap
裏面的 Entry
數組也是不同的,故它們之間的 set()
或 get()
方法是互不相見的。
ThreadLocal 使用場景
在 Android 中,ThreadLocal
比較知名的一個場景就是 Looper
的使用,我們可以在 Lopper
類的源碼中找到 ThreadLocal
的影子,因爲一個線程對應一個 Looper
,這時使用 ThreadLocal
就再好不過了。那麼爲什麼要在 Looper
中使用 ThreadLocal
,而不能是別的什麼替代方案呢?假如我們不使用 ThreadLocal
,那麼我們可以使用一個全局的哈希表來維護線程和 Looper
的關係,這樣顯然不如 ThreadLocal
來的方便。一般來說,當某些數據是以線程爲作用域並且不同線程之間有不同的數據副本的話,就可以採用 ThreadLocal
。
擴展鏈接:理解Java中的ThreadLocal