Java多線程之深入解析ThreadLocal和ThreadLocalMap
ThreadLocal概述
ThreadLocal是線程變量,ThreadLocal中填充的變量屬于目前線程,該變量對其他線程而言是隔離的。ThreadLocal為變量在每個線程中都建立了一個副本,那麼每個線程可以通路自己内部的副本變量。
它具有3個特性:
線程并發:在多線程并發場景下使用。
傳遞資料:可以通過ThreadLocal在同一線程,不同元件中傳遞公共變量。
線程隔離:每個線程變量都是獨立的,不會互相影響。
在不使用ThreadLocal的情況下,變量不隔離,得到的結果具有随機性。
public class Demo {
private String variable;
public String getVariable() {
return variable;
}
public void setVariable(String variable) {
this.variable = variable;
}
public static void main(String[] args) {
Demo demo = new Demo();
for (int i = 0; i < 5; i++) {
new Thread(()->{
demo.setVariable(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
}).start();
}
}
}
輸出結果:
View Code
在不使用ThreadLocal的情況下,變量隔離,每個線程有自己專屬的本地變量variable,線程綁定了自己的variable,隻對自己綁定的變量進行讀寫操作。
private ThreadLocal<String> variable = new ThreadLocal<>();
public String getVariable() {
return variable.get();
}
public void setVariable(String variable) {
this.variable.set(variable);
}
public static void main(String[] args) {
Demo demo = new Demo();
for (int i = 0; i < 5; i++) {
new Thread(()->{
demo.setVariable(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
}).start();
}
}
synchronized和ThreadLocal的比較
上述需求,通過synchronized加鎖同樣也能實作。但是加鎖對性能和并發性有一定的影響,線程通路變量隻能排隊等候依次操作。TreadLocal不加鎖,多個線程可以并發對變量進行操作。
private String variable;
public String getVariable() {
return variable;
}
public void setVariable(String variable) {
this.variable = variable;
}
public static void main(String[] args) {
Demo demo = new Demo1();
for (int i = 0; i < 5; i++) {
new Thread(()->{
synchronized (Demo.class){
demo.setVariable(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
}
}).start();
}
}
ThreadLocal和synchronized都是用于處理多線程并發通路資源的問題。ThreadLocal是以空間換時間的思路,每個線程都擁有一份變量的拷貝,進而實作變量隔離,互相不幹擾。關注的重點是線程之間資料的互相隔離關系。synchronized是以時間換空間的思路,隻提供一個變量,線程隻能通過排隊通路。關注的是線程之間通路資源的同步性。ThreadLocal可以帶來更好的并發性,在多線程、高并發的環境中更為合适一些。
ThreadLocal使用場景
轉賬事務的例子
JDBC對于事務原子性的控制可以通過setAutoCommit(false)設定為事務手動送出,成功後commit,失敗後rollback。在多線程的場景下,在service層開啟事務時用的connection和在dao層通路資料庫的connection應該要保持一緻,是以并發時,線程隻能隔離操作自已的connection。
解決方案1:service層的connection對象作為參數傳遞給dao層使用,事務操作放在同步代碼塊中。
存在問題:傳參提高了代碼的耦合程度,加鎖降低了程式的性能。
解決方案2:當需要擷取connection對象的時候,通過ThreadLocal對象的get方法直接擷取目前線程綁定的連接配接對象使用,如果連接配接對象是空的,則去連接配接池擷取連接配接,并通過ThreadLocal對象的set方法綁定到目前線程。使用完之後調用ThreadLocal對象的remove方法解綁連接配接對象。
ThreadLocal的優勢:
可以友善地傳遞資料:儲存每個線程綁定的資料,需要的時候可以直接擷取,避免了傳參帶來的耦合。
可以保持線程間隔離:資料的隔離在并發的情況下也能保持一緻性,避免了同步的性能損失。
ThreadLocal的原理
每個ThreadLocal維護一個ThreadLocalMap,Map的Key是ThreadLocal執行個體本身,value是要存儲的值。
每個線程内部都有一個ThreadLocalMap,Map裡面存放的是ThreadLocal對象和線程的變量副本。Thread内部的Map通過ThreadLocal對象來維護,向map擷取和設定變量副本的值。不同的線程,每次擷取變量值時,隻能擷取自己對象的副本的值。實作了線程之間的資料隔離。
JDK1.8的設計相比于之前的設計(通過ThreadMap維護了多個線程和線程變量的對應關系,key是Thread對象,value是線程變量)的好處在于,每個Map存儲的Entry數量變少了,線程越多鍵值對越多。現在的鍵值對的數量是由ThreadLocal的數量決定的,一般情況下ThreadLocal的數量少于線程的數量,而且并不是每個線程都需要建立ThreadLocal變量。當Thread銷毀時,ThreadLocal也會随之銷毀,減少了記憶體的使用,之前的方案中線程銷毀後,ThreadLocalMap仍然存在。
ThreadLocal源碼解析
set方法
首先擷取線程,然後擷取線程的Map。如果Map不為空則将目前ThreadLocal的引用作為key設定到Map中。如果Map為空,則建立一個Map并設定初始值。
get方法
首先擷取目前線程,然後擷取Map。如果Map不為空,則Map根據ThreadLocal的引用來擷取Entry,如果Entry不為空,則擷取到value值,傳回。如果Map為空或者Entry為空,則初始化并擷取初始值value,然後用ThreadLocal引用和value作為key和value建立一個新的Map。
remove方法
删除目前線程中儲存的ThreadLocal對應的實體entry。
initialValue方法
該方法的第一次調用發生在當線程通過get方法通路線程的ThreadLocal值時。除非線程先調用了set方法,在這種情況下,initialValue才不會被這個線程調用。每個線程最多調用依次這個方法。
該方法隻傳回一個null,如果想要線程變量有初始值需要通過子類繼承ThreadLocal的方式去重寫此方法,通常可以通過匿名内部類的方式實作。這個方法是protected修飾的,是為了讓子類覆寫而設計的。
ThreadLocalMap源碼分析
ThreadLocalMap是ThreadLocal的靜态内部類,沒有實作Map接口,獨立實作了Map的功能,内部的Entry也是獨立實作的。
與HashMap類似,初始容量預設是16,初始容量必須是2的整數幂。通過Entry類的資料table存放資料。size是存放的數量,threshold是擴容門檻值。
Entry繼承自WeakReference,key是弱引用,其目的是将ThreadLocal對象的生命周期和線程生命周期解綁。
弱引用和記憶體洩漏
記憶體溢出:沒有足夠的記憶體供申請者提供
記憶體洩漏:程式中已動态配置設定的堆記憶體由于某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導緻程式運作速度減慢甚至系統崩潰等驗證後溝。記憶體洩漏的堆積會導緻記憶體溢出。
弱引用:垃圾回收器一旦發現了弱引用的對象,不管記憶體是否足夠,都會回收它的記憶體。
記憶體洩漏的根源是ThreadLocalMap和Thread的生命周期是一樣長的。
如果在ThreadLocalMap的key使用強引用還是無法完全避免記憶體洩漏,ThreadLocal使用完後,ThreadLocal Reference被回收,但是Map的Entry強引用了ThreadLocal,ThreadLocal就無法被回收,因為強引用鍊的存在,Entry無法被回收,最後會記憶體洩漏。
在實際情況中,ThreadLocalMap中使用的key為ThreadLocal的弱引用,value是強引用。如果ThreadLocal沒有被外部強引用的話,在垃圾回收的時候,key會被清理,value不會。這樣ThreadLocalMap就出現了為null的Entry。如果不做任何措施,value永遠不會被GC回收,就會産生記憶體洩漏。
ThreadLocalMap中考慮到這個情況,在set、get、remove操作後,會清理掉key為null的記錄(将value也置為null)。使用完ThreadLocal後最後手動調用remove方法(删除Entry)。
也就是說,使用完ThreadLocal後,線程仍然運作,如果忘記調用remove方法,弱引用比強引用可以多一層保障,弱引用的ThreadLocal會被回收,對應的value會在下一次ThreadLocalMap調用get、set、remove方法的時候被清除,進而避免了記憶體洩漏。
Hash沖突的解決
ThreadLocalMap的構造方法
構造函數建立一個長隊為16的Entry數組,然後計算firstKey的索引,存儲到table中,設定size和threshold。
firstKey.threadLocalHashCode & (INITIAL_CAPACITY-1)用來計算索引,nextHashCode是Atomicinteger類型的,Atomicinteger類是提供原子操作的Integer類,通過線程安全的方式來加減,适合高并發使用。
每次在目前值上加上一個HASH_INCREMENT值,這個值和斐波拉契數列有關,主要目的是為了讓哈希碼可以均勻的分布在2的n次方的數組裡,進而盡量的避免沖突。
當size為2的幂次的時候,hashCode & (size - 1)相當于取模運算hashCode % size,位運算比取模更高效一些。為了使用這種取模運算, 所有size必須是2的幂次。這樣一來,在保證索引不越界的情況下,減少沖突的次數。
ThreadLocalMap的set方法
ThreadLocalMao使用了線性探測法來解決沖突。線性探測法探測下一個位址,找到空的位址則插入,若整個空間都沒有空餘位址,則産生溢出。例如:長度為8的數組中,目前key的hash值是6,6的位置已經被占用了,則hash值加一,尋找7的位置,7的位置也被占用了,回到0的位置。直到可以插入為止,可以将這個數組看成一個環形數組。
原文位址
https://www.cnblogs.com/xdcat/p/13051561.html