雖然ThreadLocal與併發問題相關,可是許多程序員僅僅將它做爲一種用於「方便傳參」的工具,胖哥認爲這也許並非ThreadLocal設計的目的,它自己是爲線程安全和某些特定場景的問題而設計的。
ThreadLocal是什麼呢!
每一個ThreadLocal能夠放一個線程級別的變量,可是它自己能夠被多個線程共享使用,並且又能夠達到線程安全的目的,且絕對線程安全。
例如:java
[java] view plain copy程序員
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變量。例如,咱們能夠設計幾種對象來封裝入口參數,在接口設計時入口參數都以對象爲基礎。
也許一個類沒法表達全部的參數意思,並且那樣容易致使強耦合。
一般咱們按照業務模型分解爲幾大類型對象做爲它們的參數包裝,而且將按照對象屬性共享狀況進行抽象,在繼承關係的每個層次各自擴展相應的參數,或者說加參數就在對象中加,共享參數就在父類中定義,這樣的參數就逐步規範化了。
咱們回到正題,探討一下ThreadLocal究竟是用來作什麼的?爲此咱們探討下文中的幾個話題。spring
爲了說明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例子安全
[java] view plain copy多線程
關於這段代碼,咱們先說幾點。
◎ 定義了兩個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)
你們能夠看到輸出的線程順序並不是最初定義線程的順序,理論上能夠說明多線程應當是併發執行的,可是依然能夠保持每一個線程裏面的值是對應的,說明這些值已經達到了線程私有的目的。
不是說共享變量沒法作到線程私有嗎?它又是如何作到線程私有的呢?這就須要咱們知道一點點原理上的東西,不然用起來也沒那麼放心,請看下面的介紹。併發
從前面的操做能夠發現,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;
這種方法很容易讓人混淆,由於這個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的這種設計很完美嗎?
不是很完美,它依然有許多坑,在這裏對它容易誤導程序員當成傳參工具就再也不多提了,下面咱們來看看它的使用不當會致使什麼技術上的問題。工具
經過上面的分析,咱們能夠認識到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源碼中:
[java] view plain copy
即:每一個Thread對象都有一個ThreadLocal.ThreadLocalMap成員變量,ThreadLocal.ThreadLocalMap是一個ThreadLocal類的靜態內部類(以下所示),因此Thread類能夠進行引用.
[java] view plain copy
因此每一個線程都會有一個ThreadLocal.ThreadLocalMap對象的引用
當在ThreadLocal中進行設值的時候:
[java] view plain copy
[java] view plain copy
首先獲取當前線程的引用,而後獲取當前線程的ThreadLocal.ThreadLocalMap對象(t.threadLocals變量就是ThreadLocal.ThreadLocalMap的變量),若是該對象爲空就建立一個,以下所示:
[java] view plain copy
這個this變量就是ThreadLocal的引用,對於同一個ThreadLocal對象每一個線程都是相同的,可是每一個線程各自有一個ThreadLocal.ThreadLocalMap對象保存着各自ThreadLocal引用爲key的值,因此互不影響,並且:若是你新建一個ThreadLocal的對象,這個對象仍是保存在每一個線程同一個ThreadLocal.ThreadLocalMap對象之中,由於一個線程只有一個ThreadLocal.ThreadLocalMap對象,這個對象是在第一個ThreadLocal第一次設值的時候進行建立,如上所述的createMap方法.
[java] view plain copy
總結:
深刻研究java.lang.ThreadLocal類:http://blog.csdn.net/xiaohulunb/article/details/19603611
API說明:
ThreadLocal(),T get(),protected T initialValue(),void remove(),void set(T value)
典型實例:
1.Hiberante的Session 工具類HibernateUtil
2.經過不一樣的線程對象設置Bean屬性,保證各個線程Bean對象的獨立性。
ThreadLocal使用的通常步驟:
[plain] view plain copy
與Synchonized的對比:
[plain] view plain copy
一句話理解ThreadLocal:向ThreadLocal裏面存東西就是向它裏面的Map存東西的,而後ThreadLocal把這個Map掛到當前的線程底下,這樣Map就只屬於這個線程了。
使用ThreadLocal改進你的層次的劃分(spring事務的實現):http://blog.csdn.net/zhouyong0/article/details/7761835
源碼剖析之ThreadLocal:http://wangxinchun.iteye.com/blog/1884228
Java中的ThreadLocal源碼解析(上):http://maosidiaoxian.iteye.com/blog/1939142
ThreadLocal與synchronized:http://blog.csdn.net/yangairong1984/article/details/2294572
Java線程:深刻ThreadLocal:http://lavasoft.blog.51cto.com/62575/258459(一個ThreadLocal的模擬實現)
Java多線程(六)、ThreadLocal類:http://blog.csdn.net/lonelyroamer/article/details/7998137