大家好,我是王有志。關注王有志,一起聊技術,聊遊戲,聊在外漂泊的生活。
好久不見,不知道大家新年過得怎麼樣?有沒有痛痛快快得放鬆?是不是還能收到很多壓歲錢?好了,話不多說,我們開始今天的主題:ThreadLocal。
我收集了4個面試中出現頻率較高的關於ThreadLocal的問題:
我們先從一個「謠言」開始,通過分析ThreadLocal的原始碼,嘗試糾正「謠言」帶來的誤解,並解答上面的問題。
很多文章都在說「ThreadLocal通過拷貝共用變數的方式解決並行安全問題」,例如:
這種說法並不準確,很容易讓人誤解為ThreadLocal會拷貝共用變數。來看個例子:
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
try {
System.out.println(DATE_FORMAT.parse("2023-01-29"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
我們知道,多執行緒並行存取同一個DateFormat範例物件會產生嚴重的並行安全問題,那麼加入ThreadLocal是不是能解決並行安全問題呢?修改下程式碼:
/**
* 第一種寫法
*/
private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<>() {
@Override
protected DateFormat initialValue() {
return DATE_FORMAT;
}
};
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
try {
System.out.println(DATE_FORMAT_THREAD_LOCAL.get().parse("2023-01-29"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
估計會有很多小夥伴會說:「你這麼寫不對!《阿里巴巴Java開發手冊》中不是這麼用的!」。把書中的用法搬過來:
/**
* 第二種寫法
*/
private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
Tips:程式碼小改了一下~~
我們來看兩種寫法的差別:
ThreadLocal#initialValue
時使用共用變數DATE_FORMAT
;ThreadLocal#initialValue
時建立SimpleDateFormat物件。按照「謠言」的描述,第一種寫法會拷貝DATE_FORMAT
的副本提供給不同的執行緒使用,但從結果上來看ThreadLocal並沒有這麼做。
有的小夥伴可能會懷疑是因為DATE_FORMAT_THREAD_LOCAL
執行緒共用導致的,但別忘了第二種寫法也是執行緒共用的。
到這裡我們應該能夠猜到,第二種寫法中每個執行緒會存取不同的SimpleDateFormat範例物件,接下來我們通過原始碼一探究竟。
除了使用ThreadLocal#initialValue
外,還可以通過ThreadLocal#set
新增變數後再使用:
ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();
threadLocal.set(new SimpleDateFormat("yyyy-MM-dd"));
System.out.println(threadLocal.get().parse("2023-01-29"));
Tips:這麼寫僅僅是為了展示用法~~
使用ThreadLocal非常簡單,3步就可以完成:
無參構造器沒什麼好說的(空實現),我們從ThreadLocal#set
開始。
ThreadLocal#set
的原始碼:
public void set(T value) {,
Thread t = Thread.currentThread();
// 獲取當前執行緒的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 新增變數
map.set(this, value);
} else {
// 初始化ThreadLocalMap
createMap(t, value);
}
}
ThreadLocal#set
的原始碼非常簡單,但卻透露出了不少重要的資訊:
接著來看原始碼:
public class ThreadLocal<T> {
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
很清晰的展示出ThreadLocalMap與Thread的關係:ThreadLocalMap是Thread的成員變數,每個Thread範例物件都擁有自己的ThreadLocalMap。
另外,還記得在關於執行緒你必須知道的8個問題(上)提到Thread範例物件與執行執行緒的關係嗎?
如果從Java的層面來看,可以認為建立Thread類的範例物件就完成了執行緒的建立,而呼叫
Thread.start0
可以認為是作業系統層面的執行緒建立和啟動。
可以近似的看作是:\(Thread範例物件\approx執行執行緒\)。也就是說,屬於Thread範例物件的ThreadLocalMap也屬於每個執行執行緒。
基於以上內容,我們好像得到了一個特殊的變數作用域:屬於執行緒。
Tips:
ThreadLocalMap是ThreadLocal的內部類,程式碼也不復雜:
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
private int size = 0;
private int threshold;
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
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);
}
}
}
僅從結構和構造方法中已經能夠窺探到ThreadLocalMap的特點:
很明顯,ThreadLocalMap是雜湊表的一種實現,ThreadLocal作為Key,我們可以將ThreadLocalMap看做是「簡版」的HashMap。
Tips:
ThreadLocalMap#set
和ThreadLocalMap#getgetEntry
的實現;ThreadLocalMap#set
中儲存的是原始變數。到目前為止,無論是ThreadLocalMap#set
還是ThreadLocalMap的構造方法,都是儲存原始變數,沒有任何拷貝副本的操作。也就是說,想要通過ThreadLocal實現變數線上程間的隔離,就需要手動為每個執行緒建立自己的變數。
ThreadLocal#get
的原始碼也非常簡單:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
前面的部分很容易理解,我們看map == null
時呼叫的ThreadLocal#setInitialValue
方法:
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
ThreadLocal#setInitialValue
方法幾乎和ThreadLocal#set
一樣,但變數是通過ThreadLocal#initialValue
獲得的。如果是通過ThreadLocal#initialValue
新增變數,在第一次呼叫ThreadLocal#get
時將變數儲存到ThreadLocalMap中。
好了,到這裡我們已經可以構建出對ThreadLocal比較完整的認知了。我們先來看ThreadLocal,ThreadLocalMap和Thread三者之間的關係:
可以看到,ThreadLocal是作為ThreadLocalMap中的Key的,而ThreadLocalMap又是Thread中的成員變數,屬於每一個Thread範例物件。忘記ThreadLocalMap是ThreadLocal的內部類這層關係,整體結構就會非常清晰。
建立ThreadLocal物件並儲存資料時,會為每個Thread物件建立ThreadLocalMap物件並儲存資料,ThreadLocal物件作為Key。在每個Thread物件的生命週期內,都可以通過ThreadLocal物件存取到儲存的資料。
那麼「ThreadLocal通過拷貝共用變數的方式解決並行安全問題」是「謠言」嗎?
我認為是的。ThreadLoal不會拷貝共用變數,它能「解決」並行安全問題的原理很簡單,要求開發者為每個執行緒「發」一個變數,即變數本身就是執行緒隔離的。接近於以下寫法:
public static Date parseDate(String dateStr) throws ParseException {
return new SimpleDateFormat("yyyy-MM-dd").parse(dateStr);
}
那這還能算是ThreadLocal去解決並行安全問題嗎?
Tips:Stack Overflow上也有關於「謠言」的討論。
既然不是解決共用變數並行安全問題的,那麼ThreadLocal有什麼用?我認為最主要的功能就是跳過方法的參數列線上程內傳遞引數。舉個例子:Dubbo借鑑Netty的FastThreadLocal,搞了InternalThreadLocal,用來隱式傳遞引數。
在ThreadLocalMap的原始碼中可以看到,Entry繼承自WeakReference,並且會將ThreadLocal新增到弱參照佇列中:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
我們知道,弱參照關聯的物件只能存活到下一次GC。如果ThreadLocal沒有關聯任何強參照,只有Entry上的弱參照的話,發生一次GC後ThreadLocal就會被回收,就會存在ThreadLocalMap上關聯Entry,但Entry上沒有Key的情況:
此時Value依舊關聯在ThreadLocalMap上,但無法通過常規手段存取,造成記憶體漏失。雖然執行緒銷燬後會釋放記憶體,但線上程執行期間,始終有一塊無法存取的記憶體被佔用。
為了避免記憶體漏失,Java建議設定靜態ThreadLocal變數,保證一直存在與之關聯的強參照:
ThreadLocal instances are typically private static fields in classes.
另外,ThreadLocal自身也做了一些努力去清除這些沒有Key的Entry,如:
ThreadLocalMap#getEntry
呼叫ThreadLocalMap#getEntryAfterMiss
;ThreadLocalMap#set
呼叫ThreadLocalMap#replaceStaleEntry
。這些方法中都會嘗試清除無用的Entry,只是觸發條件較為苛刻,實際作用較小。
除此之外,開發者主動呼叫ThreadLocal#remove
清除無用變數才是正確使用ThreadLocal的方式。
除了需要關注ThreadLocal的記憶體漏失外,我們需要關注另外一種場景:執行緒池中使用ThreadLocal。
通常執行緒池不會銷燬執行緒,因此線上程池中使用ThreadLcoal,且沒有正確執行ThreadLocal#remove
的話,執行緒中會一直存在ThreadLocal關聯的Value,那麼就需要考慮清楚,這次的ThreadLocal對下一是否還適用?
ThreadLocal的內容到這裡就結束了,使用方法,實現原理,包括記憶體漏失都還是比較簡單的。不過有一點比較難搞,因為有太多人去寫「ThreadLocal通過拷貝共用變數的方式解決並行安全問題」,導致很多人認為這是ThreadLocal的核心功能,所以無法確認坐在對面的面試官是如何理解ThreadLocal的。
我也思考了「謠言」是如何產生的,大概有兩點:
第一,《阿里巴巴Java開發手冊》中使用ThreadLocal解決了DateFormat的並行安全問題,表現上看是ThreadLocal的能力,實際上是開發者自身保證了每個執行緒使用不同的DateFormat範例物件。
第二,ThreadLocal的註釋中,提到了一句「independently initialized copy of the variable.」,搞得大家以為ThreadLocal會拷貝共用變數給執行緒使用。
如果真的遇到了這樣面試官,那隻能」見人說人話「了。
好了,今天就到這裡了,Bye~~