簡介
從名稱看,ThreadLocal 也就是thread和local的組合,也就是一個thread有一個local的變量副本
ThreadLocal提供了線程的本地副本,也就是說每個線程將會擁有一個自己獨立的變量副本
方法簡潔干練,類信息以及方法列表如下
示例
在測試類中定義了一個ThreadLocal變量,用於保存String類型數據
創建了兩個線程,分別設置值,讀取值,移除后再次讀取
package test2; /** * Created by noteless on 2019/1/30. Description: */ public class T21 { //定義ThreadLocal變量 static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { Thread thread1 = new Thread(() -> { //thread1中設置值 threadLocal.set("this is thread1's local"); //獲取值 System.out.println(Thread.currentThread().getName()+": threadLocal value:" + threadLocal.get()); //移除值 threadLocal.remove(); //再次獲取 System.out.println(Thread.currentThread().getName()+": after remove threadLocal value:" + threadLocal.get()); }, "thread1"); Thread thread2 = new Thread(() -> { //thread2中設置值 threadLocal.set("this is thread2's local"); //獲取值 System.out.println(Thread.currentThread().getName()+": threadLocal value:" + threadLocal.get()); //移除值 threadLocal.remove(); //再次獲取 System.out.println(Thread.currentThread().getName()+": after remove threadLocal value:" + threadLocal.get()); }, "thread2"); thread1.start(); thread2.start(); } }
執行結果
從結果可以看得到,每個線程中可以有自己獨有的一份數據,互相沒有影響
remove之后,數據被清空
從上面示例也可以看出來一個情況:
如果兩個線程同時對一個變量進行操作,互相之間是沒有影響的,換句話說,這很顯然並不是用來解決共享變量的一些並發問題,比如多線程的協作
因為ThreadLocal的設計理念就是共享變私有,都已經私有了,還談啥共享?
比如之前的消息隊列,生產者消費者的示例中
final LinkedList<Message> messageQueue = new LinkedList<>();
如果這個LinkedList是ThreadLocal的,生產者使用一個,消費者使用一個,還協作什么呢?
但是共享變私有,如同並發變串行,或許適合解決一些場景的線程安全問題,因為看起來就如同沒有共享變量了,不共享即安全,但是他並不是為了解決線程安全問題而存在的
實現分析
在Thread中有一個threadLocals變量,類型為ThreadLocal.ThreadLocalMap
而ThreadLocalMap則是ThreadLocal的靜態內部類,他是一個設計用來保存thread local 變量的自定義的hash map
所有的操作方法都是私有的,也就是不對外暴露任何操作方法,也就是只能在ThreadLocal中使用了
此處我們不深入,就簡單理解為是一個hash map,用於保存鍵值對
也就是說Thread中有一個“hashMap”可以用來保存鍵值對
set方法
看一下ThreadLocal的set方法
在這個方法中,接受參數,類型為T的value
首先獲取當前線程,然后調用getMap(t)
這個方法也很簡單,就是直接返回Thread內部的那個“hashMap”(threadLocals是默認的訪問權限)
繼續回到set方法,如果這個map不為空,那么以this為key,value為值,也就是ThreadLocal變量作為key
如果map為空,那么進行給這個線程創建一個map ,並且將第一組值設置進去,key仍舊是這個ThreadLocal變量
簡言之:
調用一個ThreadLocal的set方法,會將:以這個ThreadLocal類型的變量為key,參數為value的這一個鍵值對,保存在Thread內部的一個“hashMap”中
get方法
在get方法內部仍舊是獲取當前線程的內部的這個“hashMap”,然后以當前對象this(ThreadLocal)作為key,進行值的獲取
我們對這兩個方法換一個思路理解:
每個線程可能運行過程中,可能會操作很多的ThreadLocal變量,怎么區分各自?
直觀的理解就是,我們想要獲取某個線程的某個ThreadLocal變量的值
一個很好的解決方法就是借助於Map進行保存,ThreadLocal變量作為key,local值作為value
假設這個map名為:threadLocalsMap,可以提供setter和getter方法進行設置和讀取,內部為
- threadLocalsMap.set(ThreadLocal key,T value)
- threadLocalsMap.get(ThreadLocal key)
這樣就是可以達到thread --- local的效果,但是是否存在一些使用不便?我們內部定義的是ThreadLocal變量,但是只是用來作為key的?是否直接通過ThreadLocal進行值的獲取更加方便呢?
怎么能夠做到數據讀取的倒置?因為畢竟值的確是保存在Thread中的
其實也很簡單,只需要內部進行轉換就好了,對於下面兩個方法,我們都需要 ThreadLocal key
threadLocalsMap.set(ThreadLocal key,T value)
threadLocalsMap.get(ThreadLocal key)
而這個key不就是這個ThreadLocal,不就是this 么
所以:
- ThreadLocal.set(T value)就內部調用threadLocalsMap.set(this,T value)
- ThreadLocal.get()就內部調用threadLocalsMap.get(this)
所以總結下就是:
- 每個Thread內部保存了一個"hashMap",key為ThreadLocal,這個線程操作了多少個ThreadLocal,就有多少個key
- 你想獲取一個ThreadLocal變量的值,就是ThreadLocal.get(),內部就是hashMap.get(this);
- 你想設置一個ThreadLocal變量的值,就是ThreadLocal.set(T value),內部就是hashMap.set(this,value);
關鍵只是在於內部的這個“hashMap”,ThreadLocal只是讀寫倒置的“殼”,可以更簡潔易用的通過這個殼進行變量的讀寫
“倒置”的紐帶,就是getMap(t)方法
remove方法
remove方法也是簡單,當前線程,獲取當前線程的hashMap,remove
初始值
再次回頭看下get方法,如果第一次調用時,指定線程並沒有threadLocals,或者根本都沒有進行過set
會發生什么?
如下圖所示,會調用setInitialValue方法
在setInitialValue方法中,會調用initialValue方法獲取初始值,如果該線程沒有threadLocals那么會創建,如果有,會使用這個初始值構造這個ThreadLocal的鍵值對
簡單說,如果沒有set過(或者壓根內部的這個threadLocals就是null的),那么她返回的值就是初始值
這個內部的initialValue方法默認的返回null,所以一個ThreadLocal如果沒有進行set操作,那么初始值為null
如何進行初始值的設定?
可以看得出來,這是一個protected方法,所以返回一個覆蓋了這個方法的子類不就好了?在子類中實現初始值的設置
在ThreadLocal中提供了一個內部類SuppliedThreadLocal,這個內部類接受一個函數式接口Supplier作為參數,通過Supplier的get方法獲取初始值
Supplier是一個典型的內置函數式接口,無入參,返回類型T,既然是函數式接口也就是可以直接使用Lambda表達式構造初始值了!!!
如何構造這個內部類,然后進而進行初始化參數的設置呢?
提供了withInitial方法,這個方法的參數就是Supplier類型,可以看到,這個方法將入參,透傳給SuppliedThreadLocal的構造方法,直接返回一個SuppliedThreadLocal
換句話說,我們不是希望能夠借助於ThreadLocal的子類,覆蓋initialValue()方法,提供初始值嗎?
這個withInitial就是能夠達成目標的一個方法!
使用withInitial方法,創建具有初始值的ThreadLocal類型的變量,從結果可以看得出來,我們沒有任何的設置,可以獲取到值
稍作改動,增加了一次set和remove,從打印結果看得出來,set后,使用的值就是我們新設置的
而一旦remove之后,那么仍舊會使用初始值
注意:
對於initialValue方法的覆蓋,其實即使沒有提供這個子類以及這個方法也都是可以的,因為本質是要返回一個子類,並且覆蓋了這個方法
我們可以自己做,也可以直接匿名類,如下所示:創建了一個ThreadLocal的子類,覆蓋了initialValue方法
ThreadLocal <類型 > threadLocalHolder =new ThreadLocal <類型> () {
public 類型 initialValue() {
return XXX;
}
};
但是很顯然,提供了子類和方法之后,我們就可以借助於Lambda表達式進行操作,更加簡介
總結:
通過set方法可以進行值的設定
通過get方法可以進行值的讀取,如果沒有進行過設置,那么將會返回null;如果使用了withInitial方法提供了初始值,將會返回初始值
通過remove方法將會移除對該值的寫入,再次調用get方法,如果使用了withInitial方法提供了初始值,將會返回初始值,否則返回null
對於get方法,很顯然如果沒有提供初始值,返回值為null,在使用時是需要注意不要引起NPE異常
ThreadLocal,thread local,每個線程一份,到底是什么意思?
他的意思是對於一個ThreadLocal類型變量,每個線程有一個對應的值,這個值的名字就是ThreadLocal類型變量的名字,值是我們set進去的變量
但是如果set設置的是共享變量,那么ThreadLocal其實本質上還是同一個對象不是么?
這句話如果有疑問的話,可以這么理解
對於同一個ThreadLocal變量a,每個線程有一個map,map中都有一個鍵值對,key為a,值為你保存的值
但是這個值,到底每個線程都是全新的?還是使用的同一個?這是你自己的問題了!!!
ThreadLocal可以做到每個線程有一個獨立的一份值,但是你非得使用共享變量將他們設置成一個,那ThreadLocal是不會保障的
這就好比一個對象,有很多引用指向他,每個線程有一個獨立的引用,但是對象根本還是只有一個
所以,從這個角度更容易理解,為什么說ThreadLocal並不是為了解決線程安全問題而設計的,因為他並不會為線程安全做什么保障,他的能力是持有多個引用,這多個引用是否能保障是多個不同的對象,你來決策
所以我們最開始說的,ThreadLocal會為每個線程創建一個變量副本的說法是不嚴謹的
是他有這個能力做到這件事情,但是到底是什么對象,還是要看你set的是什么,set本身不會對你的值進行干涉
不過我們通常就是在合適的場景下通過new對象創建,該對象在線程內使用,也不需要被別的線程訪問
如下圖所示,你放進去的是一個共享變量,他們就是同一個對象
應用場景
前面說過,對於之前生產者消費者的示例中,就不適合使用ThreadLocal,因為問題模型就是要多線程之間協作,而不是為了線程安全就將共享變量私有化
比如,銀行賬戶的存款和取款,如果借助於ThreadLocal創建了兩個賬戶對象,就會有問題的,初始值500,明明又存進來1000塊,可支配的總額還是500
那ThreadLocal適合什么場景呢?
既然是每個線程一個,自然是適合那種希望每個線程擁有一個的那種場景(好像是廢話)
一個線程中一個,也就是線程隔離,既然是一個線程一個,那么同一個線程中調用的方法也就是共享了,所以說,有時,ThreadLocal會被用來作為參數傳遞的工具
因為它能夠保障同一個線程中的值是唯一的,那么他就共享於所有的方法中,對於所有的方法來說,相當於一個全局變量了!
所以可以用來同一個線程內全局參數傳遞
不過要慎用,因為“全局變量”的使用對於維護性、易讀性都是挑戰,尤其是ThreadLocal這種線程隔離,但是方法共享的“全局變量”
如何保障必然是獨立的一個私有變量?
對於ThreadLocal無初始化設置的變量,返回值為null
所以可以進行判斷,如果返回值為null,可以進行對象的創建,這樣就可以保障每個線程有一個獨立的,唯一的,特有的變量
示例
對於JavaWeb項目,大家都了解過Session
ps:此處不對session展開介紹,打開瀏覽器輸入網址,這就會建立一個session,關閉瀏覽器,session就失效了
在這一個時間段內,一個用戶的多個請求中,共享同一個session
Session 保存了很多信息,有的需要通過 Session 獲取信息,有些又需要修改 Session 的信息
每個線程需要獨立的session,而且很多地方都需要操作 Session,存在多方法共享 Session 的需求,所以session對象需要在多個方法中共享
如果不使用 ThreadLocal,可以在每個線程內創建一個 Session對象,然后在多個方法中將他作為參數進行傳遞
很顯然,如果每次都顯式的傳遞參數,繁瑣易錯
這種場景就適合使用ThreadLocal
下面的示例就模擬了多方法共享同一個session,但是線程間session隔離的示例
public class T24 { /** * session變量定義 */ static ThreadLocal<Session> sessionThreadLocal = new ThreadLocal<>(); /** * 獲取session */ static Session getSession() { if (null == sessionThreadLocal.get()) { sessionThreadLocal.set(new Session()); } return sessionThreadLocal.get(); } /** * 移除session */ static void closeSession() { sessionThreadLocal.remove(); } /** * 模擬一個調用session的方法 */ static void fun1(Session session) { } /** * 模擬一個調用session的方法 */ static void fun2(Session session) { } public static void main(String[] args) { new Thread(() -> { fun1(getSession()); fun2(getSession()); closeSession(); }).start(); } /** * 模擬一個session */ static class Session { } }
所以,ThreadLocal最根本的使用場景應該是:
在每個線程希望有一個獨有的變量時(這個變量還很可能需要在同一個線程內共享)
避免每個線程還需要主動地去創建這個對象(如果還需要共享,也一並解決了參數來回傳遞的問題)
換句話說就是,“如何優雅的解決:線程間隔離與線程內共享的問題”,而不是說用來解決亂七八糟的線程安全問題
所以說如果有些場景你需要線程隔離,那么考慮ThreadLocal,而不是你有了什么線程安全問題需要解決,然后求助於ThreadLocal,這不是一回事
既然能夠線程內共享,自然的確是可以用來線程內全局傳參,但是不要濫用
再次注意:
ThreadLocal只是具有這樣的能力,是你能夠做到每個線程一個獨有變量,但是如果你set時,不是傳遞的new出來的新變量,也就只是理解成“每個線程不同的引用”,對象還是那個對象(有點像參數傳遞時的值傳遞,對於對象傳遞的就是引用)
內存泄漏
ThreadLocal很好地解決了線程數據隔離的問題,但是很顯然,也引入了另一個空間問題
如果線程數量很多,如果ThreadLocal類型的變量很多,將會占用非常大的空間
而對於ThreadLocal本身來說,他只是作為key,數據並不會存儲在它的內部,所以對於ThreadLocal
ThreadLocalMap內部的這個Entity的key是弱引用
如下圖所示,實線表示強引用,虛線表示弱引用
對於真實的值是保存在Thread里面的ThreadLocal.ThreadLocalMap threadLocals中的
借助於內部的這個map,通過“殼”ThreadLocal變量的get,可以獲取到這個map的真正的值,也就是說,當前線程中持有對真實值value的強引用
而對於ThreadLocal變量本身,如下代碼所示,棧中的變量與堆空間中的這個對象,也是強引用的
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
不過對於Entity來說,key是弱引用
當一系列的執行結束之后,ThreadLocal的強引用也會消亡,也就是堆與棧之間的從ThreadLocal Ref到ThreadLocal的箭頭會斷開
由於Entity中,對於key是弱引用,所以ThreadLocal變量會被回收(GC時會回收弱引用)
而對於線程來說,如果遲遲不結束,那么就會一直存在:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value的強引用,所以value遲遲得不到回收,就會可能導致內存泄漏
ThreadLocalMap的設計中已經考慮到這種情況,所以ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap里所有key為null的value
以get方法為例
一旦將value設置為null之后,就斬斷了引用於真實內存之間的引用,就能夠真正的釋放空間,防止內存泄漏
但是這只是一種被動的方式,如果這些方法都沒有被調用怎么辦?
而且現在對於多線程來說,都是使用線程池,那個線程很可能是與應用程序共生死的,怎么辦?
那你就每次使用完ThreadLocal變量之后,執行remove方法!!!!
從以上分析也看得出來,由於ThreadLocalMap的生命周期跟Thread一樣長,所以很可能導致內存泄漏,弱引用是相當於增加了一種防護手段
通過key的弱引用,以及remove方法等內置邏輯,通過合理的處理,減少了內存泄漏的可能,如果不規范,就仍舊會導致內存泄漏
總結
ThreadLocal可以用來優雅的解決線程間隔離的對象,必須主動創建的問題,借助於ThreadLocal無需在線程中顯式的創建對象,解決方案很優雅
ThreadLocal中的set方法並不會保障的確是每個線程會獲得不同的對象,你需要對邏輯進行一定的處理(比如上面的示例中的getSession方法,如果ThreadLocal 變量的get為null,那么new對象)
是否真的能夠做到線程隔離,還要看你自己的編碼實現,不過如果是共享變量,你還放到ThreadLocal中干嘛?
所以通常都是線程獨有的對象,通過new創建