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(); } } }
輸出結果:


Thread-2 Thread-2 Thread-4 Thread-4 Thread-1 Thread-2 Thread-0 Thread-2 Thread-3 Thread-3
View Code
在不使用ThreadLocal的情況下,變量隔離,每個線程有自己專屬的本地變量variable,線程綁定了自己的variable,只對自己綁定的變量進行讀寫操作。
public class Demo { 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(); } } }
輸出結果:


Thread-0 Thread-0 Thread-1 Thread-1 Thread-2 Thread-2 Thread-3 Thread-3 Thread-4 Thread-4
View Code
synchronized和ThreadLocal的比較
上述需求,通過synchronized加鎖同樣也能實現。但是加鎖對性能和並發性有一定的影響,線程訪問變量只能排隊等候依次操作。TreadLocal不加鎖,多個線程可以並發對變量進行操作。
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 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的位置。直到可以插入為止,可以將這個數組看成一個環形數組。