成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

硬核剖析ThreadLocal源碼,面試官看了直呼內(nèi)行

開發(fā) 前端
ThreadLocal是線程本地變量,就是線程的私有變量,不同線程之間相互隔離,無法共享,相當(dāng)于每個(gè)線程拷貝了一份變量的副本。

工作面試中經(jīng)常遇到ThreadLocal,但是很多同學(xué)并不了解ThreadLocal實(shí)現(xiàn)原理,到底為什么會(huì)發(fā)生內(nèi)存泄漏也是一知半解?今天一燈帶你深入剖析ThreadLocal源碼,總結(jié)ThreadLocal使用規(guī)范,解析ThreadLocal高頻面試題。

1. ThreadLocal是什么

ThreadLocal是線程本地變量,就是線程的私有變量,不同線程之間相互隔離,無法共享,相當(dāng)于每個(gè)線程拷貝了一份變量的副本。

目的就是在多線程環(huán)境中,無需加鎖,也能保證數(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)注公眾號:一燈架構(gòu)");
// 3. 從ThreadLocal中取值
String result = threadLocal.get();
System.out.println(result); // 輸出 關(guān)注公眾號:一燈架構(gòu)

// 4. 刪除ThreadLocal中的數(shù)據(jù)
threadLocal.remove();
System.out.println(threadLocal.get()); // 輸出null
}

}

ThreadLocal的用法非常簡單,創(chuàng)建ThreadLocal的時(shí)候指定泛型類型,然后就是賦值、取值、刪除值的操作。

不同線程之間,ThreadLocal數(shù)據(jù)是隔離的,測試一下:

/**
* @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è)線程,分別給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

可以看出不同線程之間的ThreadLocal數(shù)據(jù)相互隔離,互不影響,這樣的實(shí)現(xiàn)效果有哪些應(yīng)用場景呢?

3. ThreadLocal應(yīng)用場景

ThreadLocal的應(yīng)用場景主要分為兩類:

避免對象在方法之間層層傳遞,打破層次間約束。比如用戶信息,在很多地方都需要用到,層層往下傳遞,比較麻煩。這時(shí)候就可以把用戶信息放到ThreadLocal中,需要的地方可以直接使用。

拷貝對象副本,減少初始化操作,并保證數(shù)據(jù)安全。比如數(shù)據(jù)庫連接、Spring事務(wù)管理、SimpleDataFormat格式化日期,都是使用的ThreadLocal,即避免每個(gè)線程都初始化一個(gè)對象,又保證了多線程下的數(shù)據(jù)安全。

使用ThreadLocal保證SimpleDataFormat格式化日期的線程安全,代碼類似下面這樣:

/**
* @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è)線程,分別從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存儲數(shù)據(jù),而ThreadLocalMap內(nèi)部是一個(gè)數(shù)組,數(shù)組里面存儲的是Entry對象,Entry對象里面使用key-value存儲數(shù)據(jù),key是ThreadLocal實(shí)例對象本身,value是ThreadLocal的泛型對象值。

圖片

4.1 ThreadLocalMap源碼

static class ThreadLocalMap {
// Entry對象,WeakReference是弱引用,當(dāng)沒有引用指向時(shí),會(huì)被GC回收
static class Entry extends WeakReference<ThreadLocal<?>> {
// ThreadLocal泛型對象值
Object value;
// 構(gòu)造方法,傳參是key-value
// key是ThreadLocal對象實(shí)例,value是ThreadLocal泛型對象值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

// Entry數(shù)組,用來存儲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)前線程對象
Thread t = Thread.currentThread();
// 獲取此線程對象中的ThreadLocalMap對象
ThreadLocalMap map = getMap(t);
// 如果ThreadLocal已經(jīng)設(shè)過值,直接設(shè)值,否則初始化
if (map != null)
// 設(shè)值的key就是當(dāng)前ThreadLocal對象實(shí)例,value是ThreadLocal泛型對象值
map.set(this, value);
else
// 初始化ThreadLocalMap
createMap(t, value);
}

再看一下實(shí)際的set方法源碼:

// key就是當(dāng)前ThreadLocal對象實(shí)例,value是ThreadLocal泛型對象值
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開始,判斷下標(biāo)位置是否有值,
// 如果有值判斷是否等于當(dāng)前ThreadLocal對象實(shí)例,等于就覆蓋,否則繼續(xù)向后遍歷數(shù)組,直到找到空位置
for (Entry e = tab[i];
e != null;
// nextIndex 就是讓在不超過數(shù)組長度的基礎(chǔ)上,把數(shù)組的索引位置 + 1
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果等于當(dāng)前ThreadLocal對象實(shí)例,直接覆蓋
if (k == key) {
e.value = value;
return;
}
// 當(dāng)前key是null,說明ThreadLocal對象實(shí)例已經(jīng)被GC回收了,直接覆蓋
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 找到空位置,創(chuàng)建Entry對象
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是通過線性探測法解決哈希沖突的,線性探測法具體賦值流程如下:

通過key的hashcode找到數(shù)組下標(biāo)

如果數(shù)組下標(biāo)位置是空或者等于當(dāng)前ThreadLocal對象,直接覆蓋值結(jié)束

如果不是空,就繼續(xù)向下遍歷,遍歷到數(shù)組結(jié)尾后,再從頭開始遍歷,直到找到數(shù)組為空的位置,在此位置賦值結(jié)束

線性探測法這種特殊的賦值流程,導(dǎo)致取值的時(shí)候,也要走一遍類似的流程。

4.3 get方法源碼

// 從ThreadLocal從取值
public T get(){
// 獲取當(dāng)前線程對象
Thread t = Thread.currentThread();
// 獲取此線程對象中的ThreadLocalMap對象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 通過ThreadLocal實(shí)例對象作為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){
// 通過hashcode計(jì)算數(shù)組下標(biāo)位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果下標(biāo)位置對象不為空,并且等于當(dāng)前ThreadLocal實(shí)例對象,直接返回
if (e != null && e.get() == key)
return e;
else
// 如果不是,需要繼續(xù)向下遍歷Entry數(shù)組
return getEntryAfterMiss(key, i, e);
}

再看一下線性探測法特殊的取值方法:

// 如果不是,需要繼續(xù)向下遍歷Entry數(shù)組
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e){
Entry[] tab = table;
int len = tab.length;
// 循環(huán)遍歷數(shù)組,直到找到ThreadLocal對象,或者遍歷到數(shù)組為空的位置
while (e != null) {
ThreadLocal<?> k = e.get();
// 如果等于當(dāng)前ThreadLocal實(shí)例對象,表示找到了,直接返回
if (k == key)
return e;
// key是null,表示ThreadLocal實(shí)例對象已經(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é)尾,再從頭開始遍歷
private static int nextIndex(int i, int len){
return ((i + 1 < len) ? i + 1 : 0);
}

ThreadLocal的get方法流程如下:

圖片

4.4 remove方法源碼

remove方法流程跟set、get方法類似,都是遍歷數(shù)組,找到ThreadLocal實(shí)例對象后,刪除key、value,再刪除Entry對象結(jié)束。

public void remove(){
// 獲取當(dāng)前線程的ThreadLocalMap對象
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對象,才結(jié)束
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 找到后,刪除key、value,再刪除Entry對象
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}

5. ThreadLocal使用注意事項(xiàng)

使用ThreadLocal結(jié)束,一定要調(diào)用remove方法,清理掉threadLocal數(shù)據(jù)。具體流程類似下面這樣:

/**
* @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)重的問題:

導(dǎo)致內(nèi)存溢出如果線程的生命周期很長,一直往ThreadLocal中放數(shù)據(jù),卻沒有刪除,最終產(chǎn)生OOM

導(dǎo)致數(shù)據(jù)錯(cuò)亂如果使用了線程池,一個(gè)線程執(zhí)行完任務(wù)后并不會(huì)被銷毀,會(huì)繼續(xù)執(zhí)行下一個(gè)任務(wù),導(dǎo)致下個(gè)任務(wù)訪問到了上個(gè)任務(wù)的數(shù)據(jù)。

6. 常見面試題剖析

看完了ThreadLocal源碼,再回答幾道面試題,檢驗(yàn)一下學(xué)習(xí)成果怎么樣。

6.1 ThreadLocal是怎么保證數(shù)據(jù)安全性的?

ThreadLocal底層使用的ThreadLocalMap存儲數(shù)據(jù),而ThreadLocalMap是線程Thread的私有變量,不同線程之間數(shù)據(jù)隔離,所以即使ThreadLocal的set、get、remove方法沒有加鎖,也能保證線程安全。

圖片

6.2 ThreadLocal底層為什么使用數(shù)組?而不是一個(gè)對象?

因?yàn)樵谝粋€(gè)線程中可以創(chuàng)建多個(gè)ThreadLocal實(shí)例對象,所以要用數(shù)組存儲,而不是用一個(gè)對象。

6.3 ThreadLocal是怎么解決哈希沖突的?

ThreadLocal使用的線性探測法法解決哈希沖突,線性探測法法具體賦值流程如下:

通過key的hashcode找到數(shù)組下標(biāo)

如果數(shù)組下標(biāo)位置是空或者等于當(dāng)前ThreadLocal對象,直接覆蓋值結(jié)束

如果不是空,就繼續(xù)向下遍歷,遍歷到數(shù)組結(jié)尾后,再從頭開始遍歷,直到找到數(shù)組為空的位置,在此位置賦值結(jié)束

6.4 ThreadLocal為什么要用線性探測法解決哈希沖突?

我們都知道HashMap采用的是鏈地址法(也叫拉鏈法)解決哈希沖突,為什么ThreadLocal要用線性探測法解決哈希沖突?而不用鏈地址法呢?

我的猜想是可能是創(chuàng)作者偷懶、嫌麻煩,或者是ThreadLocal使用量較少,出現(xiàn)哈希沖突概率較低,不想那么麻煩。

使用鏈地址法需要引入鏈表和紅黑樹兩種數(shù)據(jù)結(jié)構(gòu),實(shí)現(xiàn)更復(fù)雜。而線性探測法沒有引入任何額外的數(shù)據(jù)結(jié)構(gòu),直接不斷遍歷數(shù)組。

結(jié)果就是,如果一個(gè)線程中使用很多個(gè)ThreadLocal,發(fā)生哈希沖突后,ThreadLocal的get、set性能急劇下降。

線性探測法相比鏈地址法優(yōu)缺點(diǎn)都很明顯:

優(yōu)點(diǎn): 實(shí)現(xiàn)簡單,無需引入額外的數(shù)據(jù)結(jié)構(gòu)。

缺點(diǎn): 發(fā)生哈希沖突后,ThreadLocal的get、set性能急劇下降。

6.5 ThreadLocalMap的key為什么要設(shè)計(jì)成弱引用?

先說一下弱引用的特點(diǎn):

弱引用的對象擁有更短暫的生命周期,在垃圾回收器線程掃描它所管轄的內(nèi)存區(qū)域的過程中,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當(dāng)前內(nèi)存空間足夠與否,都會(huì)回收它的內(nèi)存。 不過,由于垃圾回收器是一個(gè)優(yōu)先級很低的線程,因此不一定會(huì)很快發(fā)現(xiàn)那些只具有弱引用的對象。

ThreadLocalMap的key設(shè)計(jì)成弱引用后,會(huì)不會(huì)我們正在使用,就被GC回收了?

這個(gè)是不會(huì)的,因?yàn)槲覀円恢痹趶?qiáng)引用著ThreadLocal實(shí)例對象。

/**
* @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)注公眾號:一燈架構(gòu)");
// 3. 從ThreadLocal中取值
String result = threadLocal.get();
// 手動(dòng)觸發(fā)GC
System.gc();
System.out.println(result); // 輸出 關(guān)注公眾號:一燈架構(gòu)

}

}

由上面代碼中得知,如果我們一直在使用threadLocal,觸發(fā)GC后,并不會(huì)threadLocal實(shí)例對象。

ThreadLocalMap的key設(shè)計(jì)成弱引用的目的就是:

防止我們在使用完ThreadLocal后,忘了調(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)注公眾號:一燈架構(gòu)");
// 3. 使用完threadLocal,設(shè)置成null,模仿生命周期結(jié)束
threadLocal = null;
// 觸發(fā)GC,這時(shí)候ThreadLocalMap的key就會(huì)被回收,但是value還沒有被回收。
// 只有等到下次執(zhí)行g(shù)et、set方法遍歷數(shù)組,遍歷到這個(gè)位置,才會(huì)刪除這個(gè)無效的value
System.gc();
}

}

6.6 ThreadLocal為什么會(huì)出現(xiàn)內(nèi)存泄漏?

ThreadLocal出現(xiàn)內(nèi)存泄漏的原因,就是我們使用完ThreadLocal沒有執(zhí)行remove方法刪除數(shù)據(jù)。

具體是哪些數(shù)據(jù)過多導(dǎo)致的內(nèi)存泄漏呢?

一個(gè)是數(shù)組的Entry對象,Entry對象中key、value分別是ThreadLocal實(shí)例對象和泛型對象值。

因?yàn)槲覀冊谑褂肨hreadLocal的時(shí)候,總愛把ThreadLocal設(shè)置成類的靜態(tài)變量,直到線程生命周期結(jié)束,ThreadLocal對象數(shù)據(jù)才會(huì)被回收。

另一個(gè)是數(shù)組中Entry對象的value值,也就是泛型對象值。雖然ThreadLocalMap的key被設(shè)置成弱引用,會(huì)被GC回收,但是value并沒有被回收。需要等到下次執(zhí)行g(shù)et、set方法遍歷數(shù)組,遍歷到這個(gè)位置,才會(huì)刪除這個(gè)無效的value。這也是造成內(nèi)存泄漏的原因之一。

6.7 怎么實(shí)現(xiàn)父子線程共享ThreadLocal數(shù)據(jù)?

只需要InheritableThreadLocal即可,當(dāng)初始化子線程的時(shí)候,會(huì)從父線程拷貝ThreadLocal數(shù)據(jù)。

/**
* @author 一燈架構(gòu)
* @apiNote ThreadLocal示例
**/
public class ThreadLocalDemo {
// 1. 創(chuàng)建可被子線程繼承數(shù)據(jù)的ThreadLocal
static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

public static void main(String[] args){
// 2. 給ThreadLocal賦值
threadLocal.set("關(guān)注公眾號:一燈架構(gòu)");

// 3. 啟動(dòng)一個(gè)子線程,看是否能獲取到主線程數(shù)據(jù)
new Thread(() -> {
System.out.println(threadLocal.get()); // 輸出 關(guān)注公眾號:一燈架構(gòu)
}).start();

}

}

責(zé)任編輯:武曉燕 來源: 一燈架構(gòu)
相關(guān)推薦

2023-03-09 09:14:51

ChatGPTAI

2024-11-11 10:40:19

Java變量副本

2024-09-24 10:28:22

2022-11-04 08:47:52

底層算法數(shù)據(jù)

2022-04-29 06:54:48

TS 映射類型User 類型

2020-11-03 07:48:47

當(dāng)AI入職FBI

2024-03-13 07:53:57

弱引用線程工具

2022-05-23 08:43:02

BigIntJavaScript內(nèi)置對象

2015-08-13 10:29:12

面試面試官

2023-02-16 08:10:40

死鎖線程

2021-10-26 08:40:33

String Java面試題

2022-10-31 11:10:49

Javavolatile變量

2024-03-05 10:33:39

AOPSpring編程

2021-11-02 09:05:25

Redis

2024-08-22 10:39:50

@Async注解代理

2025-03-07 00:00:10

2024-02-20 14:10:55

系統(tǒng)緩存冗余

2024-03-18 14:06:00

停機(jī)Spring服務(wù)器

2021-09-07 10:44:33

Java 注解開發(fā)

2024-10-28 08:15:32

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號

主站蜘蛛池模板: 欧美成人a∨高清免费观看 老司机午夜性大片 | 亚洲国产精品人人爽夜夜爽 | 亚洲一区二区在线 | av中文字幕在线播放 | 久久99精品久久久久久 | 黄网站在线播放 | 男女精品网站 | 亚洲国产精品99久久久久久久久 | 在线观看免费国产 | 国产精品99视频 | 国产男女猛烈无遮掩视频免费网站 | 色婷婷综合久久久中字幕精品久久 | 在线国产视频观看 | 欧美色综合天天久久综合精品 | 国产成人高清视频 | 日韩2020狼一二三 | 成人免费在线小视频 | 国产91丝袜在线18 | 黄色在线播放视频 | av中文在线播放 | 国产资源在线视频 | 久久av一区 | av网站在线播放 | 欧美aaaa视频 | 中文字幕国产精品 | 精品av天堂毛片久久久借种 | 精品入口麻豆88视频 | 日韩欧美三级 | 在线免费观看a级片 | 亚洲激情视频在线 | 亚洲va在线va天堂va狼色在线 | 婷婷丁香在线视频 | 欧美日韩亚洲在线 | 爱操av | 久久久91 | 亚洲导航深夜福利涩涩屋 | 国产成人精品一区二区三区在线 | 精品国产一区二区三区在线观看 | 欧美日韩成人网 | 91文字幕巨乱亚洲香蕉 | 一本色道久久综合亚洲精品高清 |