深入解析ThreadLocal 詳解、實現原理、使用場景方法以及內存泄漏防范 多線程中篇(十七)


簡介

從名稱看,ThreadLocal 也就是thread和local的組合,也就是一個thread有一個local的變量副本
ThreadLocal提供了線程的本地副本,也就是說每個線程將會擁有一個自己獨立的變量副本
方法簡潔干練,類信息以及方法列表如下
image_5c64cced_8d

示例

在測試類中定義了一個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();
}
}
執行結果
image_5c64cced_360f
從結果可以看得到,每個線程中可以有自己獨有的一份數據,互相沒有影響
remove之后,數據被清空
 
從上面示例也可以看出來一個情況:
如果兩個線程同時對一個變量進行操作,互相之間是沒有影響的,換句話說,這很顯然並不是用來解決共享變量的一些並發問題,比如多線程的協作
因為ThreadLocal的設計理念就是共享變私有,都已經私有了,還談啥共享?
比如之前的消息隊列,生產者消費者的示例中
  final LinkedList<Message> messageQueue = new LinkedList<>();
如果這個LinkedList是ThreadLocal的,生產者使用一個,消費者使用一個,還協作什么呢?
但是共享變私有,如同並發變串行,或許適合解決一些場景的線程安全問題,因為看起來就如同沒有共享變量了,不共享即安全,但是他並不是為了解決線程安全問題而存在的

實現分析

在Thread中有一個threadLocals變量,類型為ThreadLocal.ThreadLocalMap
image_5c64cced_790c
而ThreadLocalMap則是ThreadLocal的靜態內部類,他是一個設計用來保存thread local 變量的自定義的hash map
所有的操作方法都是私有的,也就是不對外暴露任何操作方法,也就是只能在ThreadLocal中使用了
此處我們不深入,就簡單理解為是一個hash map,用於保存鍵值對
image_5c64ccee_227c
也就是說Thread中有一個“hashMap”可以用來保存鍵值對

set方法

看一下ThreadLocal的set方法
image_5c64ccee_3823
在這個方法中,接受參數,類型為T的value
首先獲取當前線程,然后調用getMap(t)
這個方法也很簡單,就是直接返回Thread內部的那個“hashMap”(threadLocals是默認的訪問權限)
image_5c64ccee_45bc
繼續回到set方法,如果這個map不為空,那么以this為key,value為值,也就是ThreadLocal變量作為key
如果map為空,那么進行給這個線程創建一個map ,並且將第一組值設置進去,key仍舊是這個ThreadLocal變量
image_5c64ccee_63f9
簡言之:
調用一個ThreadLocal的set方法,會將:以這個ThreadLocal類型的變量為key,參數為value的這一個鍵值對,保存在Thread內部的一個“hashMap”中

get方法

在get方法內部仍舊是獲取當前線程的內部的這個“hashMap”,然后以當前對象this(ThreadLocal)作為key,進行值的獲取
image_5c64ccee_5c16
我們對這兩個方法換一個思路理解:
每個線程可能運行過程中,可能會操作很多的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方法

image_5c64ccee_4373
remove方法也是簡單,當前線程,獲取當前線程的hashMap,remove

初始值

再次回頭看下get方法,如果第一次調用時,指定線程並沒有threadLocals,或者根本都沒有進行過set
會發生什么?
如下圖所示,會調用setInitialValue方法
image_5c64ccee_5b3
在setInitialValue方法中,會調用initialValue方法獲取初始值,如果該線程沒有threadLocals那么會創建,如果有,會使用這個初始值構造這個ThreadLocal的鍵值對
簡單說,如果沒有set過(或者壓根內部的這個threadLocals就是null的),那么她返回的值就是初始值
image_5c64ccee_5f9f
這個內部的initialValue方法默認的返回null,所以一個ThreadLocal如果沒有進行set操作,那么初始值為null
image_5c64ccee_12c6
如何進行初始值的設定?
可以看得出來,這是一個protected方法,所以返回一個覆蓋了這個方法的子類不就好了?在子類中實現初始值的設置
在ThreadLocal中提供了一個內部類SuppliedThreadLocal,這個內部類接受一個函數式接口Supplier作為參數,通過Supplier的get方法獲取初始值
image_5c64ccee_dbd
Supplier是一個典型的內置函數式接口,無入參,返回類型T,既然是函數式接口也就是可以直接使用Lambda表達式構造初始值了!!!
image_5c64ccee_e57
如何構造這個內部類,然后進而進行初始化參數的設置呢?
提供了withInitial方法,這個方法的參數就是Supplier類型,可以看到,這個方法將入參,透傳給SuppliedThreadLocal的構造方法,直接返回一個SuppliedThreadLocal
換句話說,我們不是希望能夠借助於ThreadLocal的子類,覆蓋initialValue()方法,提供初始值嗎?
這個withInitial就是能夠達成目標的一個方法!
image_5c64ccee_73d2
使用withInitial方法,創建具有初始值的ThreadLocal類型的變量,從結果可以看得出來,我們沒有任何的設置,可以獲取到值
image_5c64ccee_1240
稍作改動,增加了一次set和remove,從打印結果看得出來,set后,使用的值就是我們新設置的
而一旦remove之后,那么仍舊會使用初始值
image_5c64ccee_47c5
注意:
對於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對象創建,該對象在線程內使用,也不需要被別的線程訪問
如下圖所示,你放進去的是一個共享變量,他們就是同一個對象
image_5c64ccef_3c7b

應用場景

前面說過,對於之前生產者消費者的示例中,就不適合使用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是弱引用
image_5c64ccef_779
如下圖所示,實線表示強引用,虛線表示弱引用
對於真實的值是保存在Thread里面的ThreadLocal.ThreadLocalMap threadLocals中的
借助於內部的這個map,通過“殼”ThreadLocal變量的get,可以獲取到這個map的真正的值,也就是說,當前線程中持有對真實值value的強引用
而對於ThreadLocal變量本身,如下代碼所示,棧中的變量與堆空間中的這個對象,也是強引用的
  static ThreadLocal<String> threadLocal = new ThreadLocal<>();
不過對於Entity來說,key是弱引用
image_5c64ccef_3050
當一系列的執行結束之后,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方法為例
image_5c64ccef_48c7
一旦將value設置為null之后,就斬斷了引用於真實內存之間的引用,就能夠真正的釋放空間,防止內存泄漏
image_5c64ccef_751d
但是這只是一種被動的方式,如果這些方法都沒有被調用怎么辦?
而且現在對於多線程來說,都是使用線程池,那個線程很可能是與應用程序共生死的,怎么辦?
那你就每次使用完ThreadLocal變量之后,執行remove方法!!!!
從以上分析也看得出來,由於ThreadLocalMap的生命周期跟Thread一樣長,所以很可能導致內存泄漏,弱引用是相當於增加了一種防護手段
通過key的弱引用,以及remove方法等內置邏輯,通過合理的處理,減少了內存泄漏的可能,如果不規范,就仍舊會導致內存泄漏

總結

ThreadLocal可以用來優雅的解決線程間隔離的對象,必須主動創建的問題,借助於ThreadLocal無需在線程中顯式的創建對象,解決方案很優雅
ThreadLocal中的set方法並不會保障的確是每個線程會獲得不同的對象,你需要對邏輯進行一定的處理(比如上面的示例中的getSession方法,如果ThreadLocal 變量的get為null,那么new對象)
是否真的能夠做到線程隔離,還要看你自己的編碼實現,不過如果是共享變量,你還放到ThreadLocal中干嘛?
所以通常都是線程獨有的對象,通過new創建

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com