前言
相信很多同學都聽過ThreadLocal,即使沒用過也聽過。但是要仔細一問ThreadLocal是個啥,很多同學也不一定能說清楚。本篇博客就是爲了回答關於ThreadLocal的一系列靈魂拷問:ThreadLocal是個什麼?怎麼用?爲什麼要用它?它有什麼缺點?怎麼避免…
ThreadLoacl
ThreadLoacl是什麼
在瞭解ThreadLocal之前,我們先了解下什麼是線程封閉
把對象封閉在一個線程裏,即使這個對象不是線程安全的,也不會出現併發安全問題。
實現線程封閉大致有三種方式:
- Ad-hoc線程封閉:維護線程封閉性的職責完全由程序來承擔,不推薦使用
- 棧封閉:也就是局部變量
StringBuilder是線程不安全的,但是它只是個局部變量,局部變量存儲在虛擬機棧,虛擬機棧是線程隔離的,所以不會有線程安全問題(不瞭解虛擬機分區的同學可以看看我之前的博客:Java虛擬機——內存區域)。public void testThread() { StringBuilder sb = new StringBuilder(); sb.append("Hello"); }
- ThreadLocal線程封閉:簡單易用
第三種方式就是通過ThreadLocal來實現線程封閉,線程封閉的指導思想是封閉,而不是共享。說ThreadLocal是用來解決變量共享的併發安全問題,多少有些不準確。
使用
JDK1.2開始提供的java.lang.ThreadLocal
的使用方式非常簡單
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
final ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("main-thread : Hello");
Thread thread = new Thread(() -> {
// 獲取不到主線程設置的值,所以爲null
System.out.println(threadLocal.get());
threadLocal.set("sub-thread : World");
System.out.println(threadLocal.get());
});
// 啓動子線程
thread.start();
// 讓子線程先執行完成,再繼續執行主線
thread.join();
// 獲取到的是主線程設置的值,而不是子線程設置的
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println(threadLocal.get());
}
}
運行結果
null
sub-thread : World
main-thread : Hello
null
運行結果說明了ThreadLocal只能獲取本線程設置的值,也就是線程封閉。基本上,ThreadLocal對外提供的方法只有三個get()
、set(T)
、remove()
。
原理
使用方式非常簡單,所以我們來看看ThreadLocal的源碼。ThreadLocal內部定義了一個靜態ThreadLocalMap類,ThreadLocalMap內部又定義了一個Entry類,這裏只看一些主要的屬性和方法
public class ThreadLocal<T> {
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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
// 從這裏可以看出ThreadLocalMap對象是被Thread類持有的
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 內部類ThreadLocalMap
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
// 內部類Entity,實際存儲數據的地方
// Entry的key是ThreadLocal對象,不是當前線程ID或者名稱
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 注意這裏維護的是Entry數組
private Entry[] table;
}
}
根據上面的源碼,可以大致畫出ThreadLocal在虛擬機內存中的結構
實線箭頭表示強引用,虛線箭頭表示弱引用(關於對象的四種引用,可以參考我的博客:Java中四種引用)。
需要注意的是:
- ThreadLocalMap雖然是在ThreadLocal類中定義的,但是實際上被Thread持有。
- Entry的key是(虛引用的)ThreadLocal對象,而不是當前線程ID或者線程名稱。
- ThreadLocalMap中持有的是Entry數組,而不是Entry。
對於第一點,ThreadLocalMap被Thread持有是爲了實現每個線程都有自己獨立的ThreadLocalMap對象,以此爲基礎,做到線程隔離。第二點和第三點理解,我們先來想一個問題,如果程序定義了多個ThreadLocal對象,內存結構應該是怎樣的?
此時再來看一下ThreadLocal.set(T)
方法:
public void set(T value) {
// 獲取當前線程對象
Thread t = Thread.currentThread();
// 根據線程對象獲取ThreadLocalMap對象(ThreadLocalMap被Thread持有)
ThreadLocalMap map = getMap(t);
// 如果ThreadLocalMap存在,則直接插入;不存在,則新建ThreadLocalMap
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
也就是說,如果程序定義了多個ThreadLocalMap,會共用一個ThreadLocalMap對象,所以內存結構應該是這樣
這個內存結構圖解釋了第二點和第三點。假設Entry中key爲當前線程ID或者名稱的話,那麼程序中定義多個ThreadLocal對象時,Entry數組中的所有Entry的key都一樣(或者說只能存一個value)。ThreadLocalMap中持有的是Entry數組,而不是Entry,則是因爲程序可定義多個ThreadLocal對象,自然需要一個數組。
內存泄漏
ThreadLocal會發生內存泄漏嗎?
答案是:會。
仔細看下ThreadLocal內存結構就會發現,Entry數組對象通過ThreadLocalMap最終被Thread持有,並且是強引用。也就是說Entry數組對象的生命週期和當前線程一樣。即使ThreadLocal對象被回收了,Entry數組對象也不一定被回收,這樣就有可能發生內存泄漏。ThreadLocal在設計的時候就提供了一些補救措施:
- Entry的key是弱引用的ThreadLocal對象,很容易被回收,導致key爲null(但是value不爲null)。所以在調用
get()
、set(T)
、remove()
等方法的時候,會自動清理key爲null的Entity。 remove()
方法就是用來清理無用對象,防止內存泄漏的。所以每次用完ThreadLocal後需要手動remove()
。
注:有些人認爲是弱引用導致了內存泄漏,其實不對的。假設把弱引用變成強引用,這樣無用的對象key和value都不爲null,反而不利於清理,只能通過remove()
方法手動清理,或者等待線程結束生命週期。
應用場景
- 維護JDBC的
java.sql.Connection
對象,因爲每個線程都需要保持特定的Connection對象。 - Web開發時,有些信息需要從controller傳到service傳到dao,甚至傳到util類。看起來非常不優雅,這時便可以使用ThreadLocal來優雅的實現。
- 包裹線程不安全的工具類,比如Random、SimpleDateFormat等
比較
有些人拿ThreadLocal和synchronized比較,其實他們的指導思想不一樣。
- synchronized是同一時間最多隻有一個線程執行,所以變量只需要存一份,算是一種時間換空間的思想
- ThreadLocal是多個線程互不影響,所以每個線程存一份變量,算是一種空間換時間的思想
總結
ThreadLocal是一種隔離的思想,當一個變量需要進行線程隔離時,就可以考慮使用ThreadLocal來優雅的實現。