Java中的線程私有變量ThreadLocal詳解
什么是ThreadLocal
首先看下ThreadLocal的使用示例:
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
threadLocal.set("本地變量1");
print("thread1");
System.out.println("線程1的本地變量的值為:"+threadLocal.get());
});
Thread thread2 = new Thread(() -> {
threadLocal.set("本地變量2");
print("thread2");
System.out.println("線程2的本地變量的值為:"+threadLocal.get());
});
thread1.start();
thread2.start();
}
public static void print(String s){
System.out.println(s+":"+threadLocal.get());
}執(zhí)行結(jié)果如下

我們從 Thread 類講起,在 Thread 類中有維護(hù)兩個 ThreadLocal.ThreadLocalMap 對象,分別是: threadLocals 和 inheritableThreadLocals 。
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
初始它們都為 null,只有在調(diào)用 ThreadLocal 類的 set 或 get 時才創(chuàng)建它們。ThreadLocalMap可以理解為線程私有的HashMap。
ThreadLoalMap是ThreadLocal中的一個靜態(tài)內(nèi)部類,類似HashMap的數(shù)據(jù)結(jié)構(gòu),但并沒有實現(xiàn)Map接口。
ThreadLoalMap中初始化了一個大小16的Entry數(shù)組,Entry對象用來保存每一個key-value鍵值對。key是ThreadLocal對象。


Entry用來保存數(shù)據(jù) ,而且還是繼承的弱引用。在Entry內(nèi)部使用ThreadLocal作為key,使用我們設(shè)置的value作為value。
ThreadLocal 原理
set()方法
當(dāng)我們調(diào)用 ThreadLocal 的 set() 方法時實際是調(diào)用了當(dāng)前線程的 ThreadLocalMap 的 set() 方法。
ThreadLocal 的 set() 方法中,會進(jìn)一步調(diào)用 Thread.currentThread() 獲得當(dāng)前線程對象 ,然后獲取到當(dāng)前線程對象的ThreadLocal,判斷是不是為空,為空就先調(diào)用 creadMap() 創(chuàng)建再 set(value) 創(chuàng)建 ThreadLocalMap 對象并添加變量。不為空就直接 set(value) 。



這種保證線程安全的方式稱為 線程封閉 。線程只能看到自己的ThreadLocal變量。線程之間是互相隔離的。
get()方法
其中get()方法用來獲取與當(dāng)前線程關(guān)聯(lián)的ThreadLocal的值,如果當(dāng)前線程沒有該ThreadLocal的值,則調(diào)用initialValue函數(shù)獲取初始值返回
所以一般我們使用時需要繼承該函數(shù),給出初始值(不重寫的話默認(rèn)返回Null)。
主要有以下幾步:
- 獲取當(dāng)前的Thread對象,通過getMap獲取Thread內(nèi)的ThreadLocalMap
- 如果map已經(jīng)存在,以當(dāng)前的ThreadLocal為鍵,獲取Entry對象,并從從Entry中取出值
- 否則,調(diào)用setInitialValue進(jìn)行初始化。
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}我們可以重寫 initialValue() ,設(shè)置初始值。
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return Integer.valueOf(0);
}
}remove()方法
最后一個需要探究的就是remove方法,它用于在map中移除一個不用的Entry。也是先計算出hash值,若是第一次沒有命中,就循環(huán)直到null,在此過程中也會調(diào)用expungeStaleEntry清除空key節(jié)點。代碼如下:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}實際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,弱引用的特點是,如果這個對象只存在弱引用,那么在下一次垃圾回收的時候必然會被清理掉。
所以如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap中使用這個 ThreadLocal 的 key 也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現(xiàn) key 為 null 的 value。出現(xiàn)內(nèi)存泄漏的問題。
在執(zhí)行 ThreadLocal 的 set、remove、rehash 等方法時,它都會掃描 key 為 null 的 Entry,如果發(fā)現(xiàn)某個 Entry 的 key 為 null,則代表它所對應(yīng)的 value 也沒有作用了,所以它就會把對應(yīng)的 value 置為 null,這樣,value 對象就可以被正常回收了。但是假設(shè) ThreadLocal 已經(jīng)不被使用了,那么實際上 set、remove、rehash 方法也不會被調(diào)用,與此同時,如果這個線程又一直存活、不終止的話,那么剛才的那個調(diào)用鏈就一直存在,也就導(dǎo)致了 value 的內(nèi)存泄漏。
ThreadLocal 的Hash算法
ThreadLocalMap 類似HashMap,它有自己的Hash算法。

private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}HASH_INCREMENT 這個數(shù)字被稱為斐波那契數(shù) 也叫 黃金分割數(shù),帶來的好處就是 hash 分布非常均勻。
每當(dāng)創(chuàng)建一個 ThreadLocal 對象,這個 ThreadLocal.nextHashCode 這個值就會增長 0x61c88647 。
講到Hash就會涉及到Hash沖突,跟HashMap通過鏈地址法不同的是,ThreadLocal是通過線性探測法/開放地址法來解決hash沖突。
ThreadLocal 1.7和1.8的區(qū)別
ThreadLocal 1.7版本的時候,entry對象的key是Thread。
1.8版本entry的key是ThreadLocal。
1.8版本的好處 :當(dāng)Thread銷毀的時候,ThreadLocalMap也會隨之銷毀,減少內(nèi)存的使用。因為ThreadLocalMap是在Thread里面的,所以只要Thread消失了,那ThreadLocalMap就不復(fù)存在了。
ThreadLocal 的問題
ThreadLocal 內(nèi)存泄露問題
在 ThreadLocalMap 中的 Entry 的 key 是對 ThreadLocal 的 WeakReference 弱引用,而 value 是強引用。
當(dāng) ThreadLocalMap 的某 ThreadLocal 對象只被弱引用,GC 發(fā)生時該對象會被清理,此時 key 為 null,但 value 為強引用不會被清理。
此時 value 將訪問不到也不被清理掉就可能會導(dǎo)致內(nèi)存泄漏。
注意構(gòu)造函數(shù)里的第一行代碼super(k),這意味著ThreadLocal對象是一個弱引用
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}因此我們使用完 ThreadLocal 后最好手動調(diào)用 remove() 方法。但其實在 ThreadLocalMap 的實現(xiàn)中以及考慮到這種情況,因此在調(diào)用 set() 、 get() 、 remove() 方法時,會清理 key 為 null 的記錄。
為什么采用了弱引用的實現(xiàn)而不是強引用呢?

注釋上有這么一段話:為了協(xié)助處理數(shù)據(jù)比較大并且生命周期比較長的場景,hash table的條目使用了WeakReference作為key。
所以,弱引用反而是為了解決內(nèi)存存儲問題而專門使用的。
實際上,采用弱引用反而多了一層保障,ThreadLocal被清理后key為null,對應(yīng)的value在下一次ThreadLocalMap調(diào)用set、get,就算忘記調(diào)用 remove 方法,弱引用比強引用可以多一層保障。
所以,內(nèi)存泄露的根本原因是是否手動清除操作,而不是弱引用。
ThreadLocal 父子線程繼承
異步場景下無法給子線程共享父線程的線程副本數(shù)據(jù),可以通過 InheritableThreadLocal 類解決這個問題。
它的原理就是子線程是通過在父線程中調(diào)用 new Thread() 創(chuàng)建的,在 Thread 的構(gòu)造方法中調(diào)用了 Thread的init 方法,在 init 方法中父線程數(shù)據(jù)會復(fù)制到子線程( ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); )。
代碼示例:
public class InheritableThreadLocalDemo {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
threadLocal.set("父類數(shù)據(jù):threadLocal");
inheritableThreadLocal.set("父類數(shù)據(jù):inheritableThreadLocal");
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子線程獲取父類threadLocal數(shù)據(jù):" + threadLocal.get());
System.out.println("子線程獲取父類inheritableThreadLocal數(shù)據(jù):" +inheritableThreadLocal.get());
}
}).start();
}
}但是我們做異步處理都是使用線程池,線程池會復(fù)用線程會導(dǎo)致問題出現(xiàn)。我們可以使用阿里巴巴的TTL解決這個問題。
阿里巴巴的TTL:
https://github.com/alibaba/transmittable-thread-local
到此這篇關(guān)于Java中的線程私有變量ThreadLocal詳解的文章就介紹到這了,更多相關(guān)Java線程ThreadLocal內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot整合nacos報錯無法連接nacos的解決
這篇文章主要介紹了Springboot整合nacos報錯無法連接nacos的解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06
Java內(nèi)存模型(JMM)開發(fā)應(yīng)用
JMM既是一個規(guī)范也是一個工具,正確理解和使用JMM可以幫助我們編寫出既正確又高效的多線程程序,接下來通過本文給大家介紹Java內(nèi)存模型(JMM)開發(fā)應(yīng)用實踐記錄,感興趣的朋友跟隨小編一起看看吧2025-11-11
Java網(wǎng)絡(luò)編程之簡單的服務(wù)端客戶端應(yīng)用實例
這篇文章主要介紹了Java網(wǎng)絡(luò)編程之簡單的服務(wù)端客戶端應(yīng)用,以實例形式較為詳細(xì)的分析了java網(wǎng)絡(luò)編程的原理與服務(wù)器端客戶端的實現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-04-04
SpringBoot中實現(xiàn)訂單30分鐘自動取消的項目實踐
現(xiàn)在電子商務(wù)平臺上訂單創(chuàng)建成功,等待支付,一般會給30分鐘的時間,本文主要介紹了SpringBoot中實現(xiàn)訂單30分鐘自動取消的項目實踐,具有一定的參考價值,感興趣的可以了解一下2023-10-10
spring cloud consul注冊的服務(wù)報錯critical的解決
這篇文章主要介紹了spring cloud consul注冊的服務(wù)報錯critical的解決,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-03-03
maven倉庫中心mirrors配置多個下載中心(執(zhí)行最快的鏡像)
這篇文章主要介紹了maven倉庫中心mirrors配置多個下載中心(執(zhí)行最快的鏡像),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
Java Mybatis框架Dao層的實現(xiàn)與映射文件以及核心配置文件詳解分析
MyBatis 是一款優(yōu)秀的持久層框架,它支持自定義 SQL、存儲過程以及高級映射。MyBatis 免除了幾乎所有的 JDBC 代碼以及設(shè)置參數(shù)和獲取結(jié)果集的工作。MyBatis 可以通過簡單的 XML 或注解來配置和映射原始類型、接口和 Java POJO為數(shù)據(jù)庫中的記錄2021-10-10
Java 中 Form表單數(shù)據(jù)的兩種提交方式
本文給大家分享java中form表單數(shù)據(jù)的兩種提交方式,分別是get從制定的服務(wù)器中獲取數(shù)據(jù),pos方式提交數(shù)據(jù)給指定的服務(wù)器處理,本文給大家介紹的非常詳細(xì),需要的朋友參考下吧2016-12-12

