工作面試中經(jīng)常遇到ThreadLocal,但是很多同學(xué)并不了解ThreadLocal實(shí)現(xiàn)原理,到底為什么會(huì)發(fā)生內(nèi)存泄漏也是一知半解?今天一燈帶你深入剖析ThreadLocal源碼,總結(jié)ThreadLocal使用規(guī)范,解析ThreadLocal高頻面試題。
1. ThreadLocal是什么
ThreadLocal是線(xiàn)程本地變量,就是線(xiàn)程的私有變量,不同線(xiàn)程之間相互隔離,無(wú)法共享,相當(dāng)于每個(gè)線(xiàn)程拷貝了一份變量的副本。
目的就是在多線(xiàn)程環(huán)境中,無(wú)需加鎖,也能保證數(shù)據(jù)的安全性。
2. ThreadLocal的使用
/**
* @author 一燈架構(gòu)
* @apiNote ThreadLocal示例
**/
public class ThreadLocalDemo {
// 1. 創(chuàng)建ThreadLocal
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args){
// 2. 給ThreadLocal賦值
threadLocal.set("關(guān)注公眾號(hào):一燈架構(gòu)");
// 3. 從ThreadLocal中取值
String result = threadLocal.get();
System.out.println(result); // 輸出 關(guān)注公眾號(hào):一燈架構(gòu)
// 4. 刪除ThreadLocal中的數(shù)據(jù)
threadLocal.remove();
System.out.println(threadLocal.get()); // 輸出null
}
}
ThreadLocal的用法非常簡(jiǎn)單,創(chuàng)建ThreadLocal的時(shí)候指定泛型類(lèi)型,然后就是賦值、取值、刪除值的操作。
不同線(xiàn)程之間,ThreadLocal數(shù)據(jù)是隔離的,測(cè)試一下:
/**
* @author 一燈架構(gòu)
* @apiNote ThreadLocal示例
**/
public class ThreadLocalDemo {
// 1. 創(chuàng)建ThreadLocal
static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args){
IntStream.range(0, 5).forEach(i -> {
// 創(chuàng)建5個(gè)線(xiàn)程,分別給threadLocal賦值、取值
new Thread(() -> {
// 2. 給ThreadLocal賦值
threadLocal.set(i);
// 3. 從ThreadLocal中取值
System.out.println(Thread.currentThread().getName()
+ "," + threadLocal.get());
}).start();
});
}
}
輸出結(jié)果:
Thread-2,2
Thread-4,4
Thread-1,1
Thread-0,0
Thread-3,3
可以看出不同線(xiàn)程之間的ThreadLocal數(shù)據(jù)相互隔離,互不影響,這樣的實(shí)現(xiàn)效果有哪些應(yīng)用場(chǎng)景呢?
3. ThreadLocal應(yīng)用場(chǎng)景
ThreadLocal的應(yīng)用場(chǎng)景主要分為兩類(lèi):
避免對(duì)象在方法之間層層傳遞,打破層次間約束。比如用戶(hù)信息,在很多地方都需要用到,層層往下傳遞,比較麻煩。這時(shí)候就可以把用戶(hù)信息放到ThreadLocal中,需要的地方可以直接使用。
拷貝對(duì)象副本,減少初始化操作,并保證數(shù)據(jù)安全。比如數(shù)據(jù)庫(kù)連接、Spring事務(wù)管理、SimpleDataFormat格式化日期,都是使用的ThreadLocal,即避免每個(gè)線(xiàn)程都初始化一個(gè)對(duì)象,又保證了多線(xiàn)程下的數(shù)據(jù)安全。
使用ThreadLocal保證SimpleDataFormat格式化日期的線(xiàn)程安全,代碼類(lèi)似下面這樣:
/**
* @author 一燈架構(gòu)
* @apiNote ThreadLocal示例
**/
public class ThreadLocalDemo {
// 1. 創(chuàng)建ThreadLocal
static ThreadLocal<SimpleDateFormat> threadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static void main(String[] args){
IntStream.range(0, 5).forEach(i -> {
// 創(chuàng)建5個(gè)線(xiàn)程,分別從threadLocal取出SimpleDateFormat,然后格式化日期
new Thread(() -> {
try {
System.out.println(threadLocal.get().parse("2022-11-11 00:00:00"));
} catch (ParseException e) {
throw new RuntimeException(e);
}
}).start();
});
}
}
4. ThreadLocal實(shí)現(xiàn)原理
ThreadLocal底層使用ThreadLocalMap存儲(chǔ)數(shù)據(jù),而ThreadLocalMap內(nèi)部是一個(gè)數(shù)組,數(shù)組里面存儲(chǔ)的是Entry對(duì)象,Entry對(duì)象里面使用key-value存儲(chǔ)數(shù)據(jù),key是ThreadLocal實(shí)例對(duì)象本身,value是ThreadLocal的泛型對(duì)象值。

4.1 ThreadLocalMap源碼
static class ThreadLocalMap {
// Entry對(duì)象,WeakReference是弱引用,當(dāng)沒(méi)有引用指向時(shí),會(huì)被GC回收
static class Entry extends WeakReference<ThreadLocal<?>> {
// ThreadLocal泛型對(duì)象值
Object value;
// 構(gòu)造方法,傳參是key-value
// key是ThreadLocal對(duì)象實(shí)例,value是ThreadLocal泛型對(duì)象值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// Entry數(shù)組,用來(lái)存儲(chǔ)ThreadLocal數(shù)據(jù)
private Entry[] table;
// 數(shù)組的默認(rèn)容量大小
private static final int INITIAL_CAPACITY = 16;
// 擴(kuò)容的閾值,默認(rèn)是數(shù)組大小的三分之二
private int threshold;
private void setThreshold(int len){
threshold = len * 2 / 3;
}
}
4.2 set方法源碼
// 給ThreadLocal設(shè)值
public void set(T value){
// 獲取當(dāng)前線(xiàn)程對(duì)象
Thread t = Thread.currentThread();
// 獲取此線(xiàn)程對(duì)象中的ThreadLocalMap對(duì)象
ThreadLocalMap map = getMap(t);
// 如果ThreadLocal已經(jīng)設(shè)過(guò)值,直接設(shè)值,否則初始化
if (map != null)
// 設(shè)值的key就是當(dāng)前ThreadLocal對(duì)象實(shí)例,value是ThreadLocal泛型對(duì)象值
map.set(this, value);
else
// 初始化ThreadLocalMap
createMap(t, value);
}
再看一下實(shí)際的set方法源碼:
// key就是當(dāng)前ThreadLocal對(duì)象實(shí)例,value是ThreadLocal泛型對(duì)象值
private void set(ThreadLocal<?> key, Object value){
// 獲取ThreadLocalMap中的Entry數(shù)組
Entry[] tab = table;
int len = tab.length;
// 計(jì)算key在數(shù)組中的下標(biāo),也就是ThreadLocal的hashCode和數(shù)組大小-1取余
int i = key.threadLocalHashCode & (len - 1);
// 查找流程:從下標(biāo)i開(kāi)始,判斷下標(biāo)位置是否有值,
// 如果有值判斷是否等于當(dāng)前ThreadLocal對(duì)象實(shí)例,等于就覆蓋,否則繼續(xù)向后遍歷數(shù)組,直到找到空位置
for (Entry e = tab[i];
e != null;
// nextIndex 就是讓在不超過(guò)數(shù)組長(zhǎng)度的基礎(chǔ)上,把數(shù)組的索引位置 + 1
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果等于當(dāng)前ThreadLocal對(duì)象實(shí)例,直接覆蓋
if (k == key) {
e.value = value;
return;
}
// 當(dāng)前key是null,說(shuō)明ThreadLocal對(duì)象實(shí)例已經(jīng)被GC回收了,直接覆蓋
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 找到空位置,創(chuàng)建Entry對(duì)象
tab[i] = new Entry(key, value);
int sz = ++size;
// 當(dāng)數(shù)組大小大于等于擴(kuò)容閾值(數(shù)組大小的三分之二)時(shí),進(jìn)行擴(kuò)容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
set方法具體流程如下:

從源碼和流程圖中得知,ThreadLocal是通過(guò)線(xiàn)性探測(cè)法解決哈希沖突的,線(xiàn)性探測(cè)法具體賦值流程如下:
通過(guò)key的hashcode找到數(shù)組下標(biāo)
如果數(shù)組下標(biāo)位置是空或者等于當(dāng)前ThreadLocal對(duì)象,直接覆蓋值結(jié)束
如果不是空,就繼續(xù)向下遍歷,遍歷到數(shù)組結(jié)尾后,再?gòu)念^開(kāi)始遍歷,直到找到數(shù)組為空的位置,在此位置賦值結(jié)束
線(xiàn)性探測(cè)法這種特殊的賦值流程,導(dǎo)致取值的時(shí)候,也要走一遍類(lèi)似的流程。
4.3 get方法源碼
// 從ThreadLocal從取值
public T get(){
// 獲取當(dāng)前線(xiàn)程對(duì)象
Thread t = Thread.currentThread();
// 獲取此線(xiàn)程對(duì)象中的ThreadLocalMap對(duì)象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 通過(guò)ThreadLocal實(shí)例對(duì)象作為key,在Entry數(shù)組中查找數(shù)據(jù)
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果不為空,表示找到了,直接返回
if (e != null) {
T result = (T)e.value;
return result;
}
}
// 如果ThreadLocalMap是null,就執(zhí)行初始化ThreadLocalMap操作
return setInitialValue();
}
再看一下具體的遍歷Entry數(shù)組的邏輯:
// 具體的遍歷Entry數(shù)組的方法
private Entry getEntry(ThreadLocal<?> key){
// 通過(guò)hashcode計(jì)算數(shù)組下標(biāo)位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果下標(biāo)位置對(duì)象不為空,并且等于當(dāng)前ThreadLocal實(shí)例對(duì)象,直接返回
if (e != null && e.get() == key)
return e;
else
// 如果不是,需要繼續(xù)向下遍歷Entry數(shù)組
return getEntryAfterMiss(key, i, e);
}
再看一下線(xiàn)性探測(cè)法特殊的取值方法:
// 如果不是,需要繼續(xù)向下遍歷Entry數(shù)組
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e){
Entry[] tab = table;
int len = tab.length;
// 循環(huán)遍歷數(shù)組,直到找到ThreadLocal對(duì)象,或者遍歷到數(shù)組為空的位置
while (e != null) {
ThreadLocal<?> k = e.get();
// 如果等于當(dāng)前ThreadLocal實(shí)例對(duì)象,表示找到了,直接返回
if (k == key)
return e;
// key是null,表示ThreadLocal實(shí)例對(duì)象已經(jīng)被GC回收,就幫忙清除value
if (k == null)
expungeStaleEntry(i);
else
// 索引位置+1,表示繼續(xù)向下遍歷
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
// 索引位置+1,表示繼續(xù)向下遍歷,遍歷到數(shù)組結(jié)尾,再?gòu)念^開(kāi)始遍歷
private static int nextIndex(int i, int len){
return ((i + 1 < len) ? i + 1 : 0);
}
ThreadLocal的get方法流程如下:

4.4 remove方法源碼
remove方法流程跟set、get方法類(lèi)似,都是遍歷數(shù)組,找到ThreadLocal實(shí)例對(duì)象后,刪除key、value,再刪除Entry對(duì)象結(jié)束。
public void remove(){
// 獲取當(dāng)前線(xiàn)程的ThreadLocalMap對(duì)象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
// 具體的刪除方法
private void remove(ThreadLocal<?> key){
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// 計(jì)算數(shù)組下標(biāo)
int i = key.threadLocalHashCode & (len - 1);
// 遍歷數(shù)組,直到找到空位置,
// 或者值等于當(dāng)前ThreadLocal對(duì)象,才結(jié)束
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 找到后,刪除key、value,再刪除Entry對(duì)象
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
5. ThreadLocal使用注意事項(xiàng)
使用ThreadLocal結(jié)束,一定要調(diào)用remove方法,清理掉threadLocal數(shù)據(jù)。具體流程類(lèi)似下面這樣:
/**
* @author 一燈架構(gòu)
* @apiNote ThreadLocal示例
**/
public class ThreadLocalDemo {
// 1. 創(chuàng)建ThreadLocal
static ThreadLocal<User> threadLocal = new ThreadLocal<>();
public void method(){
try {
User user = getUser();
// 2. 給threadLocal賦值
threadLocal.set(user);
// 3. 執(zhí)行其他業(yè)務(wù)邏輯
doSomething();
} finally {
// 4. 清理threadLocal數(shù)據(jù)
threadLocal.remove();
}
}
}
如果忘了調(diào)用remove方法,可能會(huì)導(dǎo)致兩個(gè)嚴(yán)重的問(wèn)題:
導(dǎo)致內(nèi)存溢出如果線(xiàn)程的生命周期很長(zhǎng),一直往ThreadLocal中放數(shù)據(jù),卻沒(méi)有刪除,最終產(chǎn)生OOM
導(dǎo)致數(shù)據(jù)錯(cuò)亂如果使用了線(xiàn)程池,一個(gè)線(xiàn)程執(zhí)行完任務(wù)后并不會(huì)被銷(xiāo)毀,會(huì)繼續(xù)執(zhí)行下一個(gè)任務(wù),導(dǎo)致下個(gè)任務(wù)訪(fǎng)問(wèn)到了上個(gè)任務(wù)的數(shù)據(jù)。
6. 常見(jiàn)面試題剖析
看完了ThreadLocal源碼,再回答幾道面試題,檢驗(yàn)一下學(xué)習(xí)成果怎么樣。
6.1 ThreadLocal是怎么保證數(shù)據(jù)安全性的?
ThreadLocal底層使用的ThreadLocalMap存儲(chǔ)數(shù)據(jù),而ThreadLocalMap是線(xiàn)程Thread的私有變量,不同線(xiàn)程之間數(shù)據(jù)隔離,所以即使ThreadLocal的set、get、remove方法沒(méi)有加鎖,也能保證線(xiàn)程安全。

6.2 ThreadLocal底層為什么使用數(shù)組?而不是一個(gè)對(duì)象?
因?yàn)樵谝粋€(gè)線(xiàn)程中可以創(chuàng)建多個(gè)ThreadLocal實(shí)例對(duì)象,所以要用數(shù)組存儲(chǔ),而不是用一個(gè)對(duì)象。
6.3 ThreadLocal是怎么解決哈希沖突的?
ThreadLocal使用的線(xiàn)性探測(cè)法法解決哈希沖突,線(xiàn)性探測(cè)法法具體賦值流程如下:
通過(guò)key的hashcode找到數(shù)組下標(biāo)
如果數(shù)組下標(biāo)位置是空或者等于當(dāng)前ThreadLocal對(duì)象,直接覆蓋值結(jié)束
如果不是空,就繼續(xù)向下遍歷,遍歷到數(shù)組結(jié)尾后,再?gòu)念^開(kāi)始遍歷,直到找到數(shù)組為空的位置,在此位置賦值結(jié)束
6.4 ThreadLocal為什么要用線(xiàn)性探測(cè)法解決哈希沖突?
我們都知道HashMap采用的是鏈地址法(也叫拉鏈法)解決哈希沖突,為什么ThreadLocal要用線(xiàn)性探測(cè)法解決哈希沖突?而不用鏈地址法呢?
我的猜想是可能是創(chuàng)作者偷懶、嫌麻煩,或者是ThreadLocal使用量較少,出現(xiàn)哈希沖突概率較低,不想那么麻煩。
使用鏈地址法需要引入鏈表和紅黑樹(shù)兩種數(shù)據(jù)結(jié)構(gòu),實(shí)現(xiàn)更復(fù)雜。而線(xiàn)性探測(cè)法沒(méi)有引入任何額外的數(shù)據(jù)結(jié)構(gòu),直接不斷遍歷數(shù)組。
結(jié)果就是,如果一個(gè)線(xiàn)程中使用很多個(gè)ThreadLocal,發(fā)生哈希沖突后,ThreadLocal的get、set性能急劇下降。
線(xiàn)性探測(cè)法相比鏈地址法優(yōu)缺點(diǎn)都很明顯:
優(yōu)點(diǎn): 實(shí)現(xiàn)簡(jiǎn)單,無(wú)需引入額外的數(shù)據(jù)結(jié)構(gòu)。
缺點(diǎn): 發(fā)生哈希沖突后,ThreadLocal的get、set性能急劇下降。
6.5 ThreadLocalMap的key為什么要設(shè)計(jì)成弱引用?
先說(shuō)一下弱引用的特點(diǎn):
弱引用的對(duì)象擁有更短暫的生命周期,在垃圾回收器線(xiàn)程掃描它所管轄的內(nèi)存區(qū)域的過(guò)程中,一旦發(fā)現(xiàn)了只具有弱引用的對(duì)象,不管當(dāng)前內(nèi)存空間足夠與否,都會(huì)回收它的內(nèi)存。 不過(guò),由于垃圾回收器是一個(gè)優(yōu)先級(jí)很低的線(xiàn)程,因此不一定會(huì)很快發(fā)現(xiàn)那些只具有弱引用的對(duì)象。
ThreadLocalMap的key設(shè)計(jì)成弱引用后,會(huì)不會(huì)我們正在使用,就被GC回收了?
這個(gè)是不會(huì)的,因?yàn)槲覀円恢痹趶?qiáng)引用著ThreadLocal實(shí)例對(duì)象。
/**
* @author 一燈架構(gòu)
* @apiNote ThreadLocal示例
**/
public class ThreadLocalDemo {
// 1. 創(chuàng)建ThreadLocal
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args){
// 2. 給ThreadLocal賦值
threadLocal.set("關(guān)注公眾號(hào):一燈架構(gòu)");
// 3. 從ThreadLocal中取值
String result = threadLocal.get();
// 手動(dòng)觸發(fā)GC
System.gc();
System.out.println(result); // 輸出 關(guān)注公眾號(hào):一燈架構(gòu)
}
}
由上面代碼中得知,如果我們一直在使用threadLocal,觸發(fā)GC后,并不會(huì)threadLocal實(shí)例對(duì)象。
ThreadLocalMap的key設(shè)計(jì)成弱引用的目的就是:
防止我們?cè)谑褂猛闠hreadLocal后,忘了調(diào)用remove方法刪除數(shù)據(jù),導(dǎo)致數(shù)組中ThreadLocal數(shù)據(jù)一直不被回收。
/**
* @author 一燈架構(gòu)
* @apiNote ThreadLocal示例
**/
public class ThreadLocalDemo {
// 1. 創(chuàng)建ThreadLocal
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args){
// 2. 給ThreadLocal賦值
threadLocal.set("關(guān)注公眾號(hào):一燈架構(gòu)");
// 3. 使用完threadLocal,設(shè)置成null,模仿生命周期結(jié)束
threadLocal = null;
// 觸發(fā)GC,這時(shí)候ThreadLocalMap的key就會(huì)被回收,但是value還沒(méi)有被回收。
// 只有等到下次執(zhí)行g(shù)et、set方法遍歷數(shù)組,遍歷到這個(gè)位置,才會(huì)刪除這個(gè)無(wú)效的value
System.gc();
}
}
6.6 ThreadLocal為什么會(huì)出現(xiàn)內(nèi)存泄漏?
ThreadLocal出現(xiàn)內(nèi)存泄漏的原因,就是我們使用完ThreadLocal沒(méi)有執(zhí)行remove方法刪除數(shù)據(jù)。
具體是哪些數(shù)據(jù)過(guò)多導(dǎo)致的內(nèi)存泄漏呢?
一個(gè)是數(shù)組的Entry對(duì)象,Entry對(duì)象中key、value分別是ThreadLocal實(shí)例對(duì)象和泛型對(duì)象值。
因?yàn)槲覀冊(cè)谑褂肨hreadLocal的時(shí)候,總愛(ài)把ThreadLocal設(shè)置成類(lèi)的靜態(tài)變量,直到線(xiàn)程生命周期結(jié)束,ThreadLocal對(duì)象數(shù)據(jù)才會(huì)被回收。
另一個(gè)是數(shù)組中Entry對(duì)象的value值,也就是泛型對(duì)象值。雖然ThreadLocalMap的key被設(shè)置成弱引用,會(huì)被GC回收,但是value并沒(méi)有被回收。需要等到下次執(zhí)行g(shù)et、set方法遍歷數(shù)組,遍歷到這個(gè)位置,才會(huì)刪除這個(gè)無(wú)效的value。這也是造成內(nèi)存泄漏的原因之一。
6.7 怎么實(shí)現(xiàn)父子線(xiàn)程共享ThreadLocal數(shù)據(jù)?
只需要InheritableThreadLocal即可,當(dāng)初始化子線(xiàn)程的時(shí)候,會(huì)從父線(xiàn)程拷貝ThreadLocal數(shù)據(jù)。
/**
* @author 一燈架構(gòu)
* @apiNote ThreadLocal示例
**/
public class ThreadLocalDemo {
// 1. 創(chuàng)建可被子線(xiàn)程繼承數(shù)據(jù)的ThreadLocal
static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args){
// 2. 給ThreadLocal賦值
threadLocal.set("關(guān)注公眾號(hào):一燈架構(gòu)");
// 3. 啟動(dòng)一個(gè)子線(xiàn)程,看是否能獲取到主線(xiàn)程數(shù)據(jù)
new Thread(() -> {
System.out.println(threadLocal.get()); // 輸出 關(guān)注公眾號(hào):一燈架構(gòu)
}).start();
}
}