Java ThreadLocal 詳解
儘管ThreadLocal與併發問題相關,可是很多程式猿只將它作為一種用於“方便傳參”的工具,筆者覺得這或許並非ThreadLocal設計的目的,它本身是為執行緒安全和某些特定場景的問題而設計的。
ThreadLocal是什麼呢。
每一個ThreadLocal能夠放一個執行緒級別的變數,可是它本身能夠被多個執行緒共享使用,並且又能夠達到執行緒安全的目的,且絕對執行緒安全。
比如:
public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();
RESOURCE代表一個能夠存放String型別的ThreadLocal物件。此時不論什麼一個執行緒能夠併發訪問這個變數,對它進行寫入、讀取操作,都是執行緒安全的。比方一個執行緒通過RESOURCE.set(“aaaa”);將資料寫入ThreadLocal中,在不論什麼一個地方,都能夠通過RESOURCE.get();將值獲取出來。
可是它也並不完美,有很多缺陷,就像大家依賴於它來做引數傳遞一樣。接下來我們就來分析它的一些不好的地方。
為什麼有些時候會將ThreadLocal作為方便傳遞引數的方式呢?比如當很多方法相互呼叫時,最初的設計可能沒有想太多,有多少個引數就傳遞多少個變數,那麼整個引數傳遞的過程就是零散的。進一步思考:若A方法呼叫B方法傳遞了8個引數。B方法接下來呼叫C方法->D方法->E方法->F方法等僅僅須要5個引數,此時在設計API時就涉及5個引數的入口。這些方法在業務發展的過程中被很多地方所複用。
某一天。我們發現F方法須要加一個引數,這個引數在A方法的入口引數中有,此時,假設要改中間方法牽涉面會非常大。並且不知道改動後會不會有Bug。
作為程式猿的我們可能會隨性一想,ThreadLocal反正是全域性的,就放這裡吧。確實好解決。
可是此時你會發現系統中這樣的方式有點像在貼補丁。越貼越多,我們必需要求呼叫相關的程式碼都使用ThreadLocal傳遞這個引數,有可能會搞得亂七八糟的。換句話說,並非不讓用。而是我們要明白它的入口和出口是可控的。
詭異的ThreadLocal最難琢磨的是“作用域”,尤其是在程式碼設計之初非常亂的情況下,假設再新增很多ThreadLocal。系統就會逐漸變成神龍見首不見尾的情況。有了這樣一個省事的東西。可能很多小夥伴更加不在意設計,由於大家都覺得這些問題都能夠通過變化的手段來解決。筆者覺得這是一種惡性迴圈。
對於這類業務場景。應當提前有所準備。須要粗粒度化業務模型。即使要用ThreadLocal,也不是加一個引數就加一個ThreadLocal變數。比如,我們能夠設計幾種物件來封裝入口引數,在介面設計時入口引數都以物件為基礎。
或許一個類無法表達全部的引數意思,並且那樣easy導致強耦合。
通常我們依照業務模型分解為幾大類型物件作為它們的引數包裝,而且將依照物件屬性共享情況進行抽象,在繼承關係的每個層次各自擴充套件對應的引數,或者說加引數就在物件中加,共享引數就在父類中定義,這種引數就逐步規範化了。
我們回到正題,探討一下ThreadLocal究竟是用來做什麼的?為此我們探討下文中的幾個話題。
(1)應用場景及使用方式
為了說明ThreadLocal的應用場景。我們來看一個框架的樣例。Spring的事務管理器通過AOP切入業務程式碼,在進入業務程式碼前,會依據相應的事務管理器提取出相應的事務物件,假如事務管理器是DataSourceTransactionManager,就會從DataSource中獲取一個連線物件,通過一定的包裝後將其儲存在ThreadLocal中。而且Spring也將DataSource進行了包裝,重寫了當中的getConnection()方法,或者說該方法的返回將由Spring來控制,這樣Spring就能讓執行緒內多次獲取到的Connection物件是同一個。
為什麼要放在ThreadLocal裡面呢?由於Spring在AOP後並不能嚮應用程式傳遞引數。應用程式的每一個業務程式碼是事先定義好的,Spring並不會要求在業務程式碼的入口引數中必須編寫Connection的入口引數。此時Spring選擇了ThreadLocal,通過它保證連線物件始終線上程內部,不論什麼時候都能拿到,此時Spring很清楚什麼時候回收這個連線,也就是很清楚什麼時候從ThreadLocal中刪除這個元素(在9.2節中會具體解說)。
從Spring事務管理器的設計上能夠看出。Spring利用ThreadLocal得到了一個非常完美的設計思路,同一時候它在設計時也十分清楚ThreadLocal中元素應該在什麼時候刪除。由此,我們簡單地覺得ThreadLocal儘量使用在一個全域性的設計上。而不是一種打補丁的間接方法。
瞭解了基本應用場景後,接下來看一個樣例。定義一個類用於存放靜態的ThreadLocal物件,通過多個執行緒並行地對ThreadLocal物件進行set、get操作,並將值進行列印。來看看每一個執行緒自己設定進去的值和取出來的值是否是一樣的。
程式碼例如以下:
程式碼清單5-8 簡單的ThreadLocal樣例
public class ThreadLocalTest {
static class ResourceClass {
public final static ThreadLocal<String> RESOURCE_1 =
new ThreadLocal<String>();
public final static ThreadLocal<String> RESOURCE_2 =
new ThreadLocal<String>();
}
static class A {
public void setOne(String value) {
ResourceClass.RESOURCE_1.set(value);
}
public void setTwo(String value) {
ResourceClass.RESOURCE_2.set(value);
}
}
static class B {
public void display() {
System.out.println(ResourceClass.RESOURCE_1.get()
+ ":" + ResourceClass.RESOURCE_2.get());
}
}
public static void main(String []args) {
final A a = new A();
final B b = new B();
for(int i = 0 ; i < 15 ; i ++) {
final String resouce1 = "執行緒-" + I;
final String resouce2 = " value = (" + i + ")";
new Thread() {
public void run() {
try {
a.setOne(resouce1);
a.setTwo(resouce2);
b.display();
}finally {
ResourceClass.RESOURCE_1.remove();
ResourceClass.RESOURCE_2.remove();
}
}
}.start();
}
}
}
關於這段程式碼,我們先說幾點。
◎ 定義了兩個ThreadLocal變數。終於的目的就是要看最後兩個值能否相應上。這樣才有機會證明ThreadLocal所儲存的資料可能是執行緒私有的。
◎ 使用兩個內部類僅僅是為了使測試簡單,方便大家直觀理解,大家也能夠將這個樣例的程式碼拆分到多個類中,得到的結果是同樣的。
◎ 測試程式碼更像是為了方便傳遞引數。由於它確實傳遞引數非常方便,但這不過為了測試。
◎ 在finally裡面有remove()操作,是為了清空資料而使用的。
為何要清空資料,在後文中會繼續介紹細節。
測試結果例如以下:
執行緒-6: value = (6)
執行緒-9: value = (9)
執行緒-0: value = (0)
執行緒-10: value = (10)
執行緒-12: value = (12)
執行緒-14: value = (14)
執行緒-11: value = (11)
執行緒-3: value = (3)
執行緒-5: value = (5)
執行緒-13: value = (13)
執行緒-2: value = (2)
執行緒-4: value = (4)
執行緒-8: value = (8)
執行緒-7: value = (7)
執行緒-1: value = (1)
大家能夠看到輸出的執行緒順序並不是最初定義執行緒的順序,理論上能夠說明多執行緒應當是併發執行的,可是依舊能夠保持每一個執行緒裡面的值是相應的,說明這些值已經達到了執行緒私有的目的。
不是說共享變數無法做到執行緒私有嗎?它又是怎樣做到執行緒私有的呢?這就須要我們知道一點點原理上的東西。否則用起來也沒那麼放心,請看以下的介紹。
(2)ThreadLocal內在原理
從前面的操作能夠發現,ThreadLocal最常見的操作就是set、get、remove三個動作。以下來看看這三個動作究竟做了什麼事情。首先看set操作,原始碼片段如圖5-5所看到的。
圖5-5 ThreadLcoal.set原始碼片段
圖5-5中的第一條程式碼取出了當前執行緒t。然後呼叫getMap(t)方法時傳入了當前執行緒,換句話說。該方法返回的ThreadLocalMap和當前執行緒有點關係,我們先記錄下來。
進一步判定假設這個map不為空。那麼設定到Map中的Key就是this。值就是外部傳入的引數。這個this是什麼呢?就是定義的ThreadLocal物件。
程式碼中有兩條路徑須要追蹤,各自是getMap(Thread)和createMap(Thread , T)。首先來看看getMap(t)操作。如圖5-6所看到的。
圖5-6 getMap(Thread)操作
在這裡。我們看到ThreadLocalMap事實上就是執行緒裡面的一個屬性,它在Thread類中的定義是:
ThreadLocal.ThreadLocalMap threadLocals = null;
這樣的方法非常easy讓人混淆,由於這個ThreadLocalMap是ThreadLocal裡面的內部類。放在了Thread類裡面作為一個屬性而存在。ThreadLocal本身成為這個Map裡面存放的Key,使用者輸入的值是Value。
太亂了。理不清楚了,畫個圖來看看(見圖5-7)。
簡單來講,就是這個Map物件在Thread裡面作為私有的變數而存在,所以是執行緒安全的。ThreadLocal通過Thread.currentThread()獲取當前的執行緒就能得到這個Map物件。同一時候將自身作為Key發起寫入和讀取,因為將自身作為Key,所以一個ThreadLocal物件就能存放一個執行緒中相應的Java物件。通過get也自然能找到這個物件。
圖5-7 Thread與ThreadLocal的虛擬碼關聯關係
假設還沒有理解,則能夠將思維放寬一點。
當定義變數String a時,這個“a”事實上僅僅是一個名稱(在第3章中已經說到了常量池),虛擬機器須要通過符號表來找到對應的資訊,而這樣的方式正好就像一種K-V結構,底層的處理方式也確實非常接近這樣。這裡的處理方式是顯式地使用Map來存放資料,這也是一種實現手段的變通。
如今有了思路。繼續回到上面的話題,為了驗證前面的判斷和理解,來看看createMap方法的細節,如圖5-8所看到的。
圖5-8 createMap操作
這段程式碼是執行一個建立新的Map的操作。而且將第一個值作為這個Map的初始化值,因為這個Map是執行緒私有的。不可能有還有一個執行緒同一時候也在對它做put操作,因此這裡的賦值和初始化是絕對執行緒安全的,也同一時候保證了每個外部寫入的值都將寫入到Map物件中。
最後來看看get()、remove()程式碼,也許看到這裡就能夠認定我們的理論是正確的。如圖5-9所看到的。
圖5-9 get()/remove()方法的程式碼片段
給我們的感覺是,這樣實現是一種技巧,而不是一種技術。
事實上是技巧還是技術全然是從某種角度來看的。或者說是從某種抽象層次來看的,假設這段程式碼在C++中實現,難道就叫技術,不是技巧了嗎?當然不是。筆者覺得技術依舊是建立在思想和方法基礎上的,僅僅是看實現的抽象層次在什麼級別。就像在本書中多個地方探討的一些基礎原理一樣,我們探討了它的思想,事實上它的實現也是基於某種技巧和手段的,僅僅是對程式封裝後就變成了某種語法和API,因此筆者覺得,一旦學會使用技巧思考問題,就學會了通過技巧去看待技術本身。我們應當通過這樣的設計,學會一種變通和發散的思維。學會理解各種各樣的場景。這樣便能夠積累很多真正的財富,這些財富不是通過某些工具的使用或測試就能夠獲得的。
ThreadLocal的這樣的設計非常完美嗎?
不是非常完美,它依舊有很多坑,在這裡對它easy誤導程式猿當成傳參工具就不再多提了。以下我們來看看它的使用不當會導致什麼技術上的問題。
(3)ThreadLocal的坑
通過上面的分析。我們能夠認識到ThreadLocal事實上是與執行緒繫結的一個變數,如此就會出現一個問題:假設沒有將ThreadLocal內的變數刪除(remove)或替換,它的生命週期將會與執行緒共存。
因此,ThreadLocal的一個非常大的“坑”就是當使用不當時,導致使用者不知道它的作用域範圍。
大家可能覺得執行緒結束後ThreadLocal應該就回收了。假設執行緒真的登出了確實是這種,可是事實有可能並不是如此。比如線上程池中對執行緒管理都是採用執行緒複用的方法(Web容器通常也會採用執行緒池)。線上程池中執行緒非常難結束甚至於永遠不會結束。這將意味著執行緒持續的時間將不可預測,甚至與JVM的生命週期一致。
那麼對應的ThreadLocal變數的生命週期也將不可預測。
或許系統中定義少量幾個ThreadLocal變數也無所謂。由於每次set資料時是用ThreadLocal本身作為Key的,同樣的Key肯定會替換原來的資料。原來的資料就能夠被釋放了,理論上不會導致什麼問題。但世事無絕對,假設ThreadLocal中直接或間接包裝了集合類或複雜物件,每次在同一個ThreadLocal中取出物件後,再對內容做操作,那麼內部的集合類和複雜物件所佔用的空間可能會開始膨脹。
拋開程式碼本身的問題。舉一個極端的樣例。假設不想定義太多的ThreadLocal變數,就用一個HashMap來存放,這貌似沒什麼問題。由於ThreadLocal在程式的不論什麼一個地方都能夠用得到,在某些設計不當的程式碼中非常難知道這個HashMap寫入的源頭,在程式碼中為了保險起見。一般會先檢查這個HashMap是否存在,若不存在,則建立一個HashMap寫進去。若存在,通常也不會替換掉。由於程式碼編寫者一般會“害怕”由於這樣的替換會丟掉一些來自“其它地方寫入HashMap的資料”。從而導致很多不可預見的問題。
在這種情況下。HashMap第一次放入ThreadLocal中或許就一直不會被釋放,而這個HashMap中可能開始存放很多Key-Value資訊,假設業務上存放的Key值在不斷變化(比如,將業務的ID作為Key),那麼這個HashMap就開始不斷變長,並且非常可能在每一個執行緒中都有一個這種HashMap,逐漸地形成了間接的記憶體洩漏。以前有非常多人吃過這個虧,並且吃虧的時候發現這種程式碼可能不是在自己的業務系統中。而是出如今某些二方包、三方包中(開源並不保證沒有問題)。
要處理這樣的問題非常複雜,只是首先要保證自己編寫的程式碼是沒問題的。要保證沒問題不是說我們不去用ThreadLocal。甚至不去學習它。由於它肯定有其應用價值。在使用時要明確ThreadLocal最難以捉摸的是“不知道哪裡是源頭”(一般是程式碼設計不當導致的),僅僅有知道了源頭才幹控制結束的部分。或者說我們從設計的角度要讓ThreadLocal的set、remove有始有終,通常在外部呼叫的程式碼中使用finally來remove資料,僅僅要我們細緻思考和抽象是能夠達到這個目的的。有些是二方包、三方包的問題,對於這些問題我們須要學會的是找到問題的根源後解決,關於二方包、三方包的執行跟蹤,可參看第3.7.9節介紹的BTrace工具。
補充:在不論什麼非同步程式中(包含非同步I/O、非堵塞I/O),ThreadLocal的引數傳遞是不靠譜的,由於執行緒將請求傳送後。就不再等待遠端返回結果繼續向下運行了,真正的返回結果得到後,處理的執行緒可能是還有一個。
#####################################總結 ####################################
Thread.java原始碼中:
ThreadLocal.ThreadLocalMap threadLocals = null;
即:每一個Thread物件都有一個ThreadLocal.ThreadLocalMap成員變數,ThreadLocal.ThreadLocalMap是一個ThreadLocal類的靜態內部類(例如以下所看到的),所以Thread類能夠進行引用.
static class ThreadLocalMap {
所以每一個執行緒都會有一個ThreadLocal.ThreadLocalMap物件的引用
當在ThreadLocal中進行設值的時候:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
首先獲取當前執行緒的引用,然後獲取當前執行緒的ThreadLocal.ThreadLocalMap物件(t.threadLocals變數就是ThreadLocal.ThreadLocalMap的變數),假設該物件為空就建立一個,例如以下所看到的:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
這個this變數就是ThreadLocal的引用,對於同一個ThreadLocal物件每一個執行緒都是同樣的,可是每一個執行緒各自有一個ThreadLocal.ThreadLocalMap物件儲存著各自ThreadLocal引用為key的值,所以互不影響,並且:假設你新建一個ThreadLocal的物件,這個物件還是儲存在每一個執行緒同一個ThreadLocal.ThreadLocalMap物件之中,由於一個執行緒僅僅有一個ThreadLocal.ThreadLocalMap物件,這個物件是在第一個ThreadLocal第一次設值的時候進行建立,如上所述的createMap方法.
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);
}
總結:
API說明:
ThreadLocal(),T get(),protected T initialValue(),void remove(),void set(T value)
典型例項:
1.Hiberante的Session 工具類HibernateUtil
2.通過不同的執行緒物件設定Bean屬性,保證各個執行緒Bean物件的獨立性。
ThreadLocal使用的一般步驟:
1、在多執行緒的類(如ThreadDemo類)中。建立一個ThreadLocal物件threadXxx,用來儲存執行緒間須要隔離處理的物件xxx。 2、在ThreadDemo類中。建立一個獲取要隔離訪問的資料的方法getXxx(),在方法中推斷,若ThreadLocal物件為null時候,應該new()一個隔離訪問型別的物件,並強制轉換為要應用的型別。 3、在ThreadDemo類的run()方法中。通過getXxx()方法獲取要操作的資料。這樣能夠保證每一個執行緒相應一個數據物件,在不論什麼時刻都操作的是這個物件。
與Synchonized的對照:
ThreadLocal和Synchonized都用於解決多執行緒併發訪問。可是ThreadLocal與synchronized有本質的差別。synchronized是利用鎖的機制,使變數或程式碼塊在某一時該僅僅能被一個執行緒訪問。而ThreadLocal為每個執行緒都提供了變數的副本,使得每個執行緒在某一時間訪問到的並非同一個物件,這樣就隔離了多個執行緒對資料的資料共享。而Synchronized卻正好相反,它用於在多個執行緒間通訊時可以獲得資料共享。 Synchronized用於執行緒間的資料共享,而ThreadLocal則用於執行緒間的資料隔離。
一句話理解ThreadLocal:向ThreadLocal裡面存東西就是向它裡面的Map存東西的,然後ThreadLocal把這個Map掛到當前的執行緒底下,這樣Map就僅僅屬於這個執行緒了。