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ù)兩個(gè) ThreadLocal.ThreadLocalMap 對(duì)象,分別是: 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 時(shí)才創(chuàng)建它們。ThreadLocalMap可以理解為線程私有的HashMap。
ThreadLoalMap是ThreadLocal中的一個(gè)靜態(tài)內(nèi)部類,類似HashMap的數(shù)據(jù)結(jié)構(gòu),但并沒有實(shí)現(xiàn)Map接口。
ThreadLoalMap中初始化了一個(gè)大小16的Entry數(shù)組,Entry對(duì)象用來保存每一個(gè)key-value鍵值對(duì)。key是ThreadLocal對(duì)象。


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



這種保證線程安全的方式稱為 線程封閉 。線程只能看到自己的ThreadLocal變量。線程之間是互相隔離的。
get()方法
其中get()方法用來獲取與當(dāng)前線程關(guān)聯(lián)的ThreadLocal的值,如果當(dāng)前線程沒有該ThreadLocal的值,則調(diào)用initialValue函數(shù)獲取初始值返回
所以一般我們使用時(shí)需要繼承該函數(shù),給出初始值(不重寫的話默認(rèn)返回Null)。
主要有以下幾步:
- 獲取當(dāng)前的Thread對(duì)象,通過getMap獲取Thread內(nèi)的ThreadLocalMap
- 如果map已經(jīng)存在,以當(dāng)前的ThreadLocal為鍵,獲取Entry對(duì)象,并從從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()方法
最后一個(gè)需要探究的就是remove方法,它用于在map中移除一個(gè)不用的Entry。也是先計(jì)算出hash值,若是第一次沒有命中,就循環(huán)直到null,在此過程中也會(huì)調(diào)用expungeStaleEntry清除空key節(jié)點(diǎn)。代碼如下:
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;
}
}
}實(shí)際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,弱引用的特點(diǎn)是,如果這個(gè)對(duì)象只存在弱引用,那么在下一次垃圾回收的時(shí)候必然會(huì)被清理掉。
所以如果 ThreadLocal 沒有被外部強(qiáng)引用的情況下,在垃圾回收的時(shí)候會(huì)被清理掉的,這樣一來 ThreadLocalMap中使用這個(gè) ThreadLocal 的 key 也會(huì)被清理掉。但是,value 是強(qiáng)引用,不會(huì)被清理,這樣一來就會(huì)出現(xiàn) key 為 null 的 value。出現(xiàn)內(nèi)存泄漏的問題。
在執(zhí)行 ThreadLocal 的 set、remove、rehash 等方法時(shí),它都會(huì)掃描 key 為 null 的 Entry,如果發(fā)現(xiàn)某個(gè) Entry 的 key 為 null,則代表它所對(duì)應(yīng)的 value 也沒有作用了,所以它就會(huì)把對(duì)應(yīng)的 value 置為 null,這樣,value 對(duì)象就可以被正?;厥樟?。但是假設(shè) ThreadLocal 已經(jīng)不被使用了,那么實(shí)際上 set、remove、rehash 方法也不會(huì)被調(diào)用,與此同時(shí),如果這個(gè)線程又一直存活、不終止的話,那么剛才的那個(gè)調(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 這個(gè)數(shù)字被稱為斐波那契數(shù) 也叫 黃金分割數(shù),帶來的好處就是 hash 分布非常均勻。
每當(dāng)創(chuàng)建一個(gè) ThreadLocal 對(duì)象,這個(gè) ThreadLocal.nextHashCode 這個(gè)值就會(huì)增長(zhǎng) 0x61c88647 。
講到Hash就會(huì)涉及到Hash沖突,跟HashMap通過鏈地址法不同的是,ThreadLocal是通過線性探測(cè)法/開放地址法來解決hash沖突。
ThreadLocal 1.7和1.8的區(qū)別
ThreadLocal 1.7版本的時(shí)候,entry對(duì)象的key是Thread。
1.8版本entry的key是ThreadLocal。
1.8版本的好處 :當(dāng)Thread銷毀的時(shí)候,ThreadLocalMap也會(huì)隨之銷毀,減少內(nèi)存的使用。因?yàn)門hreadLocalMap是在Thread里面的,所以只要Thread消失了,那ThreadLocalMap就不復(fù)存在了。
ThreadLocal 的問題
ThreadLocal 內(nèi)存泄露問題
在 ThreadLocalMap 中的 Entry 的 key 是對(duì) ThreadLocal 的 WeakReference 弱引用,而 value 是強(qiáng)引用。
當(dāng) ThreadLocalMap 的某 ThreadLocal 對(duì)象只被弱引用,GC 發(fā)生時(shí)該對(duì)象會(huì)被清理,此時(shí) key 為 null,但 value 為強(qiáng)引用不會(huì)被清理。
此時(shí) value 將訪問不到也不被清理掉就可能會(huì)導(dǎo)致內(nèi)存泄漏。
注意構(gòu)造函數(shù)里的第一行代碼super(k),這意味著ThreadLocal對(duì)象是一個(gè)弱引用
/**
* 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 后最好手動(dòng)調(diào)用 remove() 方法。但其實(shí)在 ThreadLocalMap 的實(shí)現(xiàn)中以及考慮到這種情況,因此在調(diào)用 set() 、 get() 、 remove() 方法時(shí),會(huì)清理 key 為 null 的記錄。
為什么采用了弱引用的實(shí)現(xiàn)而不是強(qiáng)引用呢?

注釋上有這么一段話:為了協(xié)助處理數(shù)據(jù)比較大并且生命周期比較長(zhǎng)的場(chǎng)景,hash table的條目使用了WeakReference作為key。
所以,弱引用反而是為了解決內(nèi)存存儲(chǔ)問題而專門使用的。
實(shí)際上,采用弱引用反而多了一層保障,ThreadLocal被清理后key為null,對(duì)應(yīng)的value在下一次ThreadLocalMap調(diào)用set、get,就算忘記調(diào)用 remove 方法,弱引用比強(qiáng)引用可以多一層保障。
所以,內(nèi)存泄露的根本原因是是否手動(dòng)清除操作,而不是弱引用。
ThreadLocal 父子線程繼承
異步場(chǎng)景下無法給子線程共享父線程的線程副本數(shù)據(jù),可以通過 InheritableThreadLocal 類解決這個(gè)問題。
它的原理就是子線程是通過在父線程中調(diào)用 new Thread() 創(chuàng)建的,在 Thread 的構(gòu)造方法中調(diào)用了 Thread的init 方法,在 init 方法中父線程數(shù)據(jù)會(huì)復(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();
}
}但是我們做異步處理都是使用線程池,線程池會(huì)復(fù)用線程會(huì)導(dǎo)致問題出現(xiàn)。我們可以使用阿里巴巴的TTL解決這個(gè)問題。
阿里巴巴的TTL:
https://github.com/alibaba/transmittable-thread-local
到此這篇關(guān)于Java中的線程私有變量ThreadLocal詳解的文章就介紹到這了,更多相關(guān)Java線程ThreadLocal內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot打包不同環(huán)境配置以及shell腳本部署的方法
這篇文章主要給大家介紹了關(guān)于springboot打包不同環(huán)境配置以及shell腳本部署的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用springboot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
詳解Spring Data JPA動(dòng)態(tài)條件查詢的寫法
本篇文章主要介紹了Spring Data JPA動(dòng)態(tài)條件查詢的寫法 ,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06
mybatis-plus中l(wèi)ambdaQuery()與lambdaUpdate()比較常見的使用方法總結(jié)
mybatis-plus是在mybatis的基礎(chǔ)上做增強(qiáng)不做改變,簡(jiǎn)化了CRUD操作,下面這篇文章主要給大家介紹了關(guān)于mybatis-plus中l(wèi)ambdaQuery()與lambdaUpdate()比較常見的使用方法,需要的朋友可以參考下2022-09-09
Java封裝數(shù)組之動(dòng)態(tài)數(shù)組實(shí)現(xiàn)方法詳解
這篇文章主要介紹了Java封裝數(shù)組之動(dòng)態(tài)數(shù)組實(shí)現(xiàn)方法,結(jié)合實(shí)例形式詳細(xì)分析了java動(dòng)態(tài)數(shù)組的實(shí)現(xiàn)原理、操作步驟與相關(guān)注意事項(xiàng),需要的朋友可以參考下2020-03-03
springcloud項(xiàng)目快速開始起始模板的實(shí)現(xiàn)
本文主要介紹了springcloud項(xiàng)目快速開始起始模板思路的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
自帶IDEA插件的阿里開源診斷神器Arthas線上項(xiàng)目BUG調(diào)試
這篇文章主要為大家介紹了自帶IDEA插件阿里開源診斷神器Arthas線上項(xiàng)目BUG調(diào)試,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
SpringMVC自定義攔截器實(shí)現(xiàn)過程詳解
這篇文章主要介紹了SpringMVC自定義攔截器實(shí)現(xiàn)過程詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05
一文詳解Java如何優(yōu)雅地判斷對(duì)象是否為空
這篇文章主要給大家介紹了關(guān)于Java如何優(yōu)雅地判斷對(duì)象是否為空的相關(guān)資料,在Java中可以使用以下方法優(yōu)雅地判斷一個(gè)對(duì)象是否為空,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-04-04
java基礎(chǔ)篇之Date類型最常用的時(shí)間計(jì)算(相當(dāng)全面)
這篇文章主要給大家介紹了關(guān)于java基礎(chǔ)篇之Date類型最常用的時(shí)間計(jì)算的相關(guān)資料,Java中的Date類是用來表示日期和時(shí)間的類,它提供了一些常用的方法來處理日期和時(shí)間的操作,需要的朋友可以參考下2023-12-12

