什麼是ThreadLocal
ThreadLocal
類顧名思義可以了解為線程本地變量。也就是說如果定義了一個
ThreadLocal
,
每個線程往這個
ThreadLocal
中讀寫是線程隔離,互相之間不會影響的。它提供了一種将可變資料通過每個線程有自己的獨立副本進而實作線程封閉的機制。
實際應用
實際開發中我們真正使用
ThreadLocal
的場景還是比較少的,大多數使用都是在架構裡面。最常見的使用場景的話就是用它來解決資料庫連接配接、
Session
管理等保證每一個線程中使用的資料庫連接配接是同一個。還有一個用的比較多的場景就是用來解決
SimpleDateFormat
解決線程不安全的問題,不過現在
java8
提供了
DateTimeFormatter
它是線程安全的,感興趣的同學可以去看看。還可以利用它進行優雅的傳遞參數,傳遞參數的時候,如果父線程生成的變量或者參數直接通過
ThreadLocal
傳遞到子線程參數就會丢失,這個後面會介紹一個其他的
ThreadLocal
來專門解決這個問題的。
ThreadLocal api介紹
ThreadLocal的API還是比較少的就幾個api

我們看下這幾個
api
的使用,使用起來也超級簡單
private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(()->"java金融");
public static void main(String[] args) {
System.out.println("擷取初始值:"+threadLocal.get());
threadLocal.set("關注:【java金融】");
System.out.println("擷取修改後的值:"+threadLocal.get());
threadLocal.remove();
}
輸出結果:
擷取初始值:java金融
擷取修改後的值:關注:【java金融】
是不是炒雞簡單,就幾行代碼就把所有
api
都覆寫了。下面我們就來簡單看看這幾個
api
的源碼吧。
成員變量
/**初始容量,必須為2的幂
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/** Entry表,大小必須為2的幂
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
這裡會有一個面試經常問到的問題:為什麼
entry
數組的大小,以及初始容量都必須是
2
的幂?對于
firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
以及很多源碼裡面都是使用
hashCode &( $2^n$-1) 來代替hashCode% $2^n$。
這種寫法好處如下:
- 使用位運算替代取模,提升計算效率。
- 為了使不同
值發生碰撞的機率更小,盡可能促使元素在哈希表中均勻地散列。hash
set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
set
方法還是比較簡單的,我們可以重點看下這個方法裡面的ThreadLocalMap,它既然是個map(注意不要與
java.util.map
混為一談,這裡指的是概念上的
map
),肯定是有自己的key和value組成,我們根據源碼可以看出它的
key
是其實可以把它簡單看成是
ThreadLocal
,但是實際上ThreadLocal中存放的是
ThreadLocal
的弱引用,而它的
value
的話是我們實際
set
的值
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value; // 實際存放的值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry
就是是
ThreadLocalMap
裡定義的節點,它繼承了
WeakReference
類,定義了一個類型為
Object
的
value
,用于存放塞到
ThreadLocal
裡的值。我們再來看下這個
ThreadLocalMap
是位于哪裡的?我們看到
ThreadLocalMap
是位于
Thread
裡面的一個變量,而我們的值又是放在
ThreadLocalMap
,這樣的話我們就實作了每個線程間的隔離。下面兩張圖的基本就把
ThreadLocal
的結構給介紹清楚了。
接下來我們再看下
ThreadLocalMap
裡面的資料結構,我們知道
HaseMap
解決
hash
沖突是由連結清單和紅黑樹(
jdk1.8
)來解決的,但是這個我們看到
ThreadLocalMap
隻有一個數組,它是怎麼來解決
hash
沖突呢?
ThreadLocalMap
采用線性探測的方式,什麼是線性探測呢?就是根據初始
key
的hashcode值确定元素在
table
數組中的位置,如果發現這個位置上已經有其他
key
值的元素被占用,則利用固定的算法尋找一定步長的下個位置,依次判斷,直至找到能夠存放的位置。
ThreadLocalMap
Hash
沖突的方式就是簡單的步長加
1
或減
1
,尋找下一個相鄰的位置。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
這種方式的話如果一個線程裡面有大量的
ThreadLocal
就會産生性能問題,因為每次都需要對這個
table
進行周遊,清空無效的值。是以我們在使用的時候盡可能的使用少的
ThreadLocal
,不要線上程裡面建立大量的
ThreadLocal
,如果需要設定不同的參數類型我們可以通過
ThreadLocal
來存放一個
Object
Map
這樣的話,可以大大減少建立
ThreadLocal
的數量。
僞代碼如下:
public final class HttpContext {
private HttpContext() {
}
private static final ThreadLocal<Map<String, Object>> CONTEXT = ThreadLocal.withInitial(() -> new ConcurrentHashMap(64));
public static <T> void add(String key, T value) {
if(StringUtils.isEmpty(key) || Objects.isNull(value)) {
throw new IllegalArgumentException("key or value is null");
}
CONTEXT.get().put(key, value);
}
public static <T> T get(String key) {
return (T) get().get(key);
}
public static Map<String, Object> get() {
return CONTEXT.get();
}
public static void remove() {
CONTEXT.remove();
}
}
這樣的話我們如果需要傳遞不同的參數,可以直接使用一個
ThreadLocal
就可以代替多個
ThreadLocal
了。
如果覺得不想這麼玩,我就是要建立多個
ThreadLocal
,我的需求就是這樣,而且性能還得要好,這個能不能實作列?可以使用
netty
FastThreadLocal
可以解決這個問題,不過要配合使
FastThreadLocalThread
或者它子類的線程線程效率才會更高,更多關于它的使用可以自行查閱資料哦。
下面我們先來看下它的這個哈希函數
// 生成hash code間隙為這個魔數,可以讓生成出來的值或者說ThreadLocal的ID較為均勻地分布在2的幂大小的數組中。
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
可以看出,它是在上一個被構造出的
ThreadLocal
ID/threadLocalHashCode
的基礎上加上一個魔數
0x61c88647
的。這個魔數的選取與斐波那契散列有關,
0x61c88647
對應的十進制為
1640531527
.當我們使用
0x61c88647
這個魔數累加對每個
ThreadLocal
配置設定各自的
ID
也就是
threadLocalHashCode
再與
2
的幂(數組的長度)取模,得到的結果分布很均勻。我們可以來也示範下通過這個魔數
public class MagicHashCode {
private static final int HASH_INCREMENT = 0x61c88647;
public static void main(String[] args) {
hashCode(16); //初始化16
hashCode(32); //後續2倍擴容
hashCode(64);
}
private static void hashCode(Integer length) {
int hashCode = 0;
for (int i = 0; i < length; i++) {
hashCode = i * HASH_INCREMENT + HASH_INCREMENT;//每次遞增HASH_INCREMENT
System.out.print(hashCode & (length - 1));
System.out.print(" ");
}
System.out.println();
}
運作結果:
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0
不得不佩服下這個作者,通過使用了斐波那契散列法,來保證哈希表的離散度,讓結果很均勻。可見代碼要寫的好,數學還是少不了啊。其他的源碼就不分析了,大家感興趣可以自行去檢視下。
ThreadLocal的記憶體洩露
關于
ThreadLocal
是否會引起記憶體洩漏也是一個比較有争議性的問題。首先我們需要知道什麼是記憶體洩露?
在Java中,記憶體洩漏就是存在一些被配置設定的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連;其次,這些對象是無用的,即程式以後不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的記憶體洩漏,這些對象不會被GC所回收,然而它卻占用記憶體。
ThreadLocal
的記憶體洩露情況:
- 線程的生命周期很長,當
沒有被外部強引用的時候就會被ThreadLocal
回收(給GC
置空了):ThreadLocal
會出現一個ThreadLocalMap
為key
null
,但這個Entry
Entry
将永遠沒辦法被通路到(後續在也無法操作value
等方法了)。如果當這個線程一直沒有結束,那這個set、get
key
null
因為也存在強引用(Entry
),而Entry.value
被目前線程的Entry
強引用(ThreadLocalMap
),導緻這個Entry[] table
永遠無法被Entry.value
GC
,造成記憶體洩漏。
下面我們來示範下這個場景
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Long []> threadLocal = new ThreadLocal<>();
for (int i = 0; i < 50; i++) {
run(threadLocal);
}
Thread.sleep(50000);
// 去除強引用
threadLocal = null;
System.gc();
System.runFinalization();
System.gc();
}
private static void run(ThreadLocal<Long []> threadLocal) {
new Thread(() -> {
threadLocal.set(new Long[1024 * 1024 *10]);
try {
Thread.sleep(1000000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
通過
jdk
自帶的工具
jconsole.exe
會發現即使執行了
gc
記憶體也不會減少,因為key還被線程強引用着。效果圖如下:
- 針對于這種情況
在設計中,已經考慮到這種情況的發生,你隻要調用ThreadLocalMap
方法都會調用了set()、get()、remove()
方法去清除cleanSomeSlots()、expungeStaleEntry()
key
null
。這是一種被動的清理方式,但是如果value
ThreadLocal
方法沒有被調用,就會導緻set(),get(),remove()
的記憶體洩漏。它的文檔推薦我們使用value
修飾的static
,導緻ThreadLocal
的生命周期和持有它的類一樣長,由于ThreadLocal
有強引用在,意味着這個ThreadLocal
不會被ThreadLocal
。在這種情況下,我們如果不手動删除,GC
Entry
永遠不為key
,弱引用也就失去了意義。是以我們在使用的時候盡可能養成一個好的習慣,使用完成後手動調用下null
方法。其實實際生産環境中我們手動remove
大多數情況并不是為了避免這種remove
key
的情況,更多的時候,是為了保證業務以及程式的正确性。比如我們下單請求後通過null
建構了訂單的上下文請求資訊,然後通過線程池異步去更新使用者積分,這時候如果更新完成,沒有進行ThreadLocal
操作,即使下一次新的訂單會覆寫原來的值但是也是有可能會導緻業務問題。remove
如果不想手動清理是否還有其他方式解決下列?
FastThreadLocal
可以去了解下,它提供了自動回收機制。
- 線上程池的場景,程式不停止,線程一直在複用的話,基本不會銷毀,其實本質就跟上面例子是一樣的。如果線程不複用,用完就銷毀了就不會存在洩露的情況。因為線程結束的時候會
主動調用jvm
方法清理。exit
/**
- This method is called by the system to give a Thread
-
a chance to clean up before it actually exits.
*/
if (group != null) { group.threadTerminated(this); group = null; } /* Aggressively null out all reference fields: see bug 4006245 */ target = null; /* Speed the release of some of these resources */ threadLocals = null; inheritableThreadLocals = null; inheritedAccessControlContext = null; blocker = null; uncaughtExceptionHandler = null;
InheritableThreadLocal
文章開頭有提到過父子之間線程的變量傳遞丢失的情況。但是
InheritableThreadLocal
提供了一種父子線程之間的資料共享機制。可以解決這個問題。
static ThreadLocal threadLocal = new ThreadLocal<>();
static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
threadLocal.set("threadLocal主線程的值");
Thread.sleep(100);
new Thread(() -> System.out.println("子線程擷取threadLocal的主線程值:" + threadLocal.get())).start();
Thread.sleep(100);
inheritableThreadLocal.set("inheritableThreadLocal主線程的值");
new Thread(() -> System.out.println("子線程擷取inheritableThreadLocal的主線程值:" + inheritableThreadLocal.get())).start();
}
輸出結果
線程擷取threadLocal的主線程值:null
子線程擷取inheritableThreadLocal的主線程值:inheritableThreadLocal主線程的值
但是
InheritableThreadLocal
和線程池使用的時候就會存在問題,因為子線程隻有線上程對象建立的時候才會把父線程
inheritableThreadLocals
中的資料複制到自己的
inheritableThreadLocals
中。這樣就實作了父線程和子線程的上下文傳遞。但是線程池的話,線程會複用,是以會存在問題。如果要解決這個問題可以有什麼辦法列?大家可以思考下,或者在下方留言哦。如果實在不想思考的話,可以參考下阿裡巴巴的
transmittable-thread-local
哦。
總結
- 大概介紹了
的常見用法,以及大緻實作原理,以及關于ThreadLocal
的記憶體洩露問題,以及關于使用它需要注意的事項,以及如何解決父子線程之間的傳遞。介紹了ThreadLocal
各種使用場景,以及需要注意的事項。本文重點介紹了ThreadLocal、InheritableThreadLocal、FastThreadLocal、transmittable-thread-local
,如果把這個弄清楚了,其他幾種ThreadLocal就更好了解了。ThreadLocal
結束
- 由于自己才疏學淺,難免會有纰漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
- 如果你覺得文章還不錯,你的轉發、分享、贊賞、點贊、留言就是對我最大的鼓勵。
- 感謝您的閱讀,十分歡迎并感謝您的關注。
巨人的肩膀摘蘋果:
https://zhuanlan.zhihu.com/p/40515974 https://www.cnblogs.com/aspirant/p/8991010.html https://www.cnblogs.com/jiangxinlingdu/p/11123538.html https://blog.csdn.net/hewenbo111/article/details/80487252