- ??作者簡介:大家好,我是愛敲代碼的小黃,獨(dú)角獸企業(yè)的Java開發(fā)工程師,CSDN博客專家,阿里云專家博主
- ??系列專欄:Java設(shè)計模式、數(shù)據(jù)結(jié)構(gòu)和算法、Kafka從入門到成神、Kafka從成神到升仙、Spring從成神到升仙系列
- ??如果感覺博主的文章還不錯的話,請??三連支持??一下博主哦
- ??博主正在努力完成2023計劃中:以夢為馬,揚(yáng)帆起航,2023追夢人
- ??聯(lián)系方式:hls1793929520,加我進(jìn)群,大家一起學(xué)習(xí),一起進(jìn)步,一起對抗互聯(lián)網(wǎng)寒冬??
從根上理解 ThreadLocal 的來龍去脈
一、引言
對于 Java
開發(fā)者而言,關(guān)于 并發(fā)編程,我們一般當(dāng)做黑盒來進(jìn)行使用,不需要去打開這個黑盒。
但隨著目前程序員行業(yè)的發(fā)展,我們有必要打開這個黑盒,去探索其中的奧妙。
本期 并發(fā)編程 解析系列文章,將帶你領(lǐng)略 并發(fā)編程 的奧秘
廢話不多說,發(fā)車!
二、概念
ThreadLocal
的英文字面意思為 “本地線程”,實(shí)際上 ThreadLocal
代表的是線程的本地變量,可能將其命名為 ThreadLocalVariable
更加容易讓人理解。
ThreadLocal
如何做到為每個線程存有一份獨(dú)立的本地值呢?
一個 ThreadLocal
實(shí)例可以形象地理解為一個 Map
(早期版本的 ThreadLocal
是這樣設(shè)計的)。
當(dāng)工作線程 Thread
實(shí)例向本地變量保持某個值時,會以 “Key-Value對”(即鍵-值對)的形式保存在 ThreadLocal
內(nèi)部的Map中,其中 Key
為線程 Thread
實(shí)例,Value
為待保存的值。
當(dāng)工作線程 Thread
實(shí)例從 ThreadLocal
本地變量取值時,會以 Thread
實(shí)例為 Key
,獲取其綁定的 Value
。
一個ThreadLocal
實(shí)例內(nèi)部結(jié)構(gòu)的形象如下所示:
后續(xù)在 JDK8
中進(jìn)行了優(yōu)化
三、使用
public class ThreadLocalTest {
ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void main(String[] args) {
ThreadLocalTest threadLocalTest = new ThreadLocalTest();
threadLocalTest.showTwoThread();
}
public void showTwoThread() {
new Thread(new Runnable() {
public void run() {
threadLocal.set("Thread1");
System.out.println("I am " + threadLocal.get());
}
}).start();
new Thread(new Runnable() {
public void run() {
threadLocal.set("Thread2");
System.out.println("I am " + threadLocal.get());
}
}).start();
}
}
上述是我們 ThreadLocal
的一個使用 Demo
最終結(jié)果輸出:
I am Thread1
I am Thread2
由此可以看到,我們在線程中使用了 ThreadLocal
做到了線程之間彼此隔離的作用。
四、源碼解析
1. set 方法
public void set(T value) {
// 獲取當(dāng)前線程
Thread t = Thread.currentThread();
// 通過當(dāng)前線程獲取 ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 驗(yàn)證當(dāng)前的ThreadLocalMap是否為空
if (map != null){
// 若不為空,則直接插入數(shù)據(jù)即可
map.set(this, value);
} else{
// 若為空,則需要創(chuàng)建 ThreadLocalMap
createMap(t, value);
}
}
// 根據(jù)當(dāng)前的Thread創(chuàng)建ThreadLocalMap并賦予初始值firstValue
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
從這一步我們可以看出,如果當(dāng)前并沒有創(chuàng)建過 ThreadLocalMap
,則會去第一次創(chuàng)建:
1.1 初始化設(shè)值
// 初始化ThreadLocalMap
// 1) 創(chuàng)建底層數(shù)據(jù)存儲的Entry數(shù)組table
// 2) 根據(jù)hashCode計算出來所在數(shù)組的下標(biāo)i執(zhí)行賦值操作
// 3) 初始化代表table中元素個數(shù)size的值
// 4) 初始化閾值threshold
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table,默認(rèn)數(shù)值為16
table = new Entry[INITIAL_CAPACITY];
// hash獲取下標(biāo)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 創(chuàng)建待插入的對象并放入數(shù)據(jù)
table[i] = new Entry(firstKey, firstValue);
// 調(diào)整容量
size = 1;
// 調(diào)整負(fù)載系數(shù)
setThreshold(INITIAL_CAPACITY);
}
但如果當(dāng)前的 ThreadLocalMap
不是第一次創(chuàng)建,則會執(zhí)行 map.set(this, value)
1.2 非初始化設(shè)值
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 首先,我們要先知曉,Entry對象是【弱引用】對象
// 注意這里循環(huán)的條件是e != null,這個很重要,它采用的就是上面講的開放地址法。
// 這里遍歷的邏輯是,先通過hash找到數(shù)組下標(biāo),然后尋找相等的ThreadLocal對象,找不到就往下一個index找。
// --------------------------------------------------------------------------------------------
// 有三種情況會跳出循環(huán):
// 1) 找到了相同key的ThreadLocal對象,然后更新value值;
// 2) 找到了數(shù)組中的一個元素Entry,但是key=null,說明虛引用是可被GC回收的狀態(tài)。
// 3) 一直往數(shù)組下一個index查找,直到下一個index對應(yīng)的元素為null;
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 拿到當(dāng)前的ThreadLocal對象
ThreadLocal<?> k = e.get();
// 如果相同的ThreadLocal,直接更新即可
if (k == key) {
e.value = value;
return;
}
// 當(dāng)前的key=null,則表示虛引用的ThreadLocal是被GC回收的狀態(tài)
if (k == null) {
// 1) 向前找到第一個空閑的key下標(biāo)為 first
// 2) 向后找到第一個空閑的key下標(biāo)為 last
// 3) 設(shè)置當(dāng)前的key-value
// 4) 【expungeStaleEntry】清除first~last之間的陳舊的Entry,直接將value置為null
replaceStaleEntry(key, value, i);
return;
}
}
// 走到這里就說明下標(biāo)為i的位置上,是沒有元素的,所以可以直接將新建的Entry元素插入到i這個位置
tab[i] = new Entry(key, value);
int sz = ++size;
// cleanSomeSlots:存在陳舊的Entry且已經(jīng)被清除
if (!cleanSomeSlots(i, sz) && sz >= threshold){
rehash();
}
}
// 循環(huán)獲取下標(biāo)
// 0-1-2-3-0-1-2-3
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private void rehash() {
// 清除陳舊的key
expungeStaleEntries();
// 擴(kuò)容
// 1) 創(chuàng)建一個長度是當(dāng)前table的2倍
// 2) 遍歷舊的table數(shù)組,依次獲得里面的Entry元素
// 2-1) 計算Entry在新的table數(shù)組中應(yīng)該插入的位置
// 2-2) 如果下標(biāo)h已經(jīng)被占用了,那么就向后查找空位,直到找到空閑的位置為止,插入進(jìn)去。
// 2-3) 如果下標(biāo)h沒有被占用,那么就插入到h的位置上
// 2-4) count++
// 3) 根據(jù)新數(shù)組的長度,更新閾值threshold
// 4) 針對新的數(shù)組,更新全局變量size和table
if (size >= threshold - threshold / 4){
resize();
}
}
1.3 流程圖
2. get方法
public T get() {
// 拿到當(dāng)前線程
Thread t = Thread.currentThread();
// 通過當(dāng)前線程獲取 ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果 ThreadLocalMap 不為空的情況下
if (map != null) {
// 得到當(dāng)前的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 初始化(和Set一樣的流程)
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
// 獲取下標(biāo)
int i = key.threadLocalHashCode & (table.length - 1);
// 獲取鍵值
Entry e = table[i];
// 如果不為空且當(dāng)前key相等
if (e != null && e.get() == key)
return e;
else
//
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
// 取當(dāng)前的table
Entry[] tab = table;
int len = tab.length;
// 循環(huán)遍歷當(dāng)前entry
while (e != null) {
ThreadLocal<?> k = e.get();
// 找到即返回
if (k == key)
return e;
if (k == null)
// 清理陳舊的key
expungeStaleEntry(i);
else
// 遍歷下一個
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
3. remove方法
public void remove() {
// 拿到當(dāng)前線程的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null){
m.remove(this);
}
}
private void remove(ThreadLocal<?> key) {
// 獲取當(dāng)前key的所屬下標(biāo)
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 循環(huán)找出當(dāng)前的key
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 若找到引用置為空并清理陳舊的entry
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
4. 開放地址法
當(dāng)我們看到這里,就發(fā)現(xiàn)這個 ThreadLocalMap 有些“奇怪”,它并沒有按照我們之前在學(xué)習(xí) HashMap
的 鏈?zhǔn)?/strong> 方式去解決哈希沖突,即:數(shù)組+鏈表。它其實(shí)使用的是一種叫做 “開放地址法” 作為解決哈希沖突的一種方式。
什么是開放地址法呢?
開放地址法的基本思想就是:一旦發(fā)生了沖突,那么就去尋找下一個空的地址;那么只要表足夠大,空的地址總能找到,并將記錄插入進(jìn)去。
ThreadLocalMap
和 HashMap
的區(qū)別是什么呢?
HashMap:
- 數(shù)據(jù)結(jié)構(gòu)是數(shù)組+鏈表
- 通過鏈地址法解決
hash
沖突的問題 - 里面的
Entry
內(nèi)部類的引用都是強(qiáng)引用
ThreadLocalMap:
- 數(shù)據(jù)結(jié)構(gòu)僅僅是數(shù)組
- 通過開放地址法來解決
hash
沖突的問題 -
Entry
內(nèi)部類中的key
是弱引用,value
是強(qiáng)引用
鏈地址法和開放地址法的優(yōu)缺點(diǎn)是什么呢?
開放地址法
- 容易產(chǎn)生堆積問題,不適于大規(guī)模的數(shù)據(jù)存儲。
- 散列函數(shù)的設(shè)計對沖突會有很大的影響,插入時可能會出現(xiàn)多次沖突的現(xiàn)象。
- 刪除的元素是多個沖突元素中的一個,需要對后面的元素作處理,實(shí)現(xiàn)較復(fù)雜。
鏈地址法
- 處理沖突簡單,且無堆積現(xiàn)象,平均查找長度短。
- 鏈表中的結(jié)點(diǎn)是動態(tài)申請的,適合構(gòu)造表不能確定長度的情況。
- 刪除結(jié)點(diǎn)的操作易于實(shí)現(xiàn)。只要簡單地刪去鏈表上相應(yīng)的結(jié)點(diǎn)即可。
- 指針需要額外的空間,故當(dāng)結(jié)點(diǎn)規(guī)模較小時,開放定址法較為節(jié)省空間。
ThreadLocalMap采用開放地址法原因是什么?
ThreadLocal
往往存放的數(shù)據(jù)量不會特別大(而且key 是弱引用又會被垃圾回收,及時讓數(shù)據(jù)量更?。?/p>
采用開放地址法簡單的結(jié)構(gòu)會更節(jié)省空間,同時數(shù)組的查詢效率也是非常高,加上第一點(diǎn)的保障,沖突概率也比較低。
5. 清理方式
5.1 探測式清除
這個清除主要是通過這個方法:expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
// 獲取數(shù)據(jù)
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
// 遍歷Table,這里是一直向后遍歷,直到遇到為null(也就是沒用過的)為止
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 賦予空值
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 重新hash
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
5.2 啟發(fā)式清除
從上面探測式清除我們可以得到一個結(jié)論:探測式清除并不能完全清除我們table的所有陳舊數(shù)據(jù)
這個時候需要 啟發(fā)式清除 的出場:
// i:探測式清除的返回地址
// n:table的總?cè)萘?/span>
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
// 下一個
i = nextIndex(i, len);
Entry e = tab[i];
// 如果這個已經(jīng)被GC掉了
if (e != null && e.get() == null) {
n = len;
removed = true;
// 由當(dāng)前地址去進(jìn)行探測式清除
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
看完說實(shí)話,實(shí)現(xiàn)有點(diǎn)操蛋,接著人家探測式清除外包了一層,就換了一個名字了
假設(shè):m = n>>>=1 ,也就是 2 的 m 次冪等于 n
當(dāng)連續(xù) m
次沒有去進(jìn)行清除操作,則默認(rèn)當(dāng)前 table
中沒有垃圾
例如:數(shù)組長度是16,那么2^4=16,也就是連續(xù)4次沒有過期Entry,即 m = logn/log2(n為數(shù)組長度)
五、內(nèi)存泄露
public class ThreadLocalForOOM {
/**
* -Xms50m -Xmx50m
*/
static class OOMObject {
private Long[] a = new Long[2 * 1024 * 1024];
}
final static ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 5, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
final static ThreadLocal<OOMObject> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
int finalI = i;
pool.execute(() -> {
threadLocal.set(new OOMObject());
System.out.println("oom object--->" + finalI);
OOMObject oomObject = threadLocal.get();
System.out.println("oomObject---->" + oomObject);
// threadLocal.remove(); // 記得remove 防止內(nèi)存泄露,此時一定要在使用完remove
});
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我們運(yùn)行上述代碼,會出現(xiàn) OOM
異常:Exception in thread "pool-1-thread-4" java.lang.OutOfMemoryError: Java heap space
我們在使用完 threadLocal
之后,需要及時的進(jìn)行 remove
刪除,加上這句就OK了。
六、總結(jié)
又是一篇大工程的文章結(jié)束了
記得校招的時候,對于 ThreadLocal
的認(rèn)知只停留在線程隔離,但并未真正的去剖析其源碼是怎么做到線程隔離的
我們通過講解 set
、get
、remove
等三大方法源碼,體會到了整個的運(yùn)行流程
而其 table擴(kuò)容
、清除方式
、解決hash沖突
的方式也令人感到眼前一亮,讀完還是有收獲的
那么如何證明你真的理解了 ThreadLocal
呢,我這里出個經(jīng)典的題目,大家可以想一下:請你聊一下 ThreadLocal
的 set
過程?
如果你能看到這,那博主必須要給你一個大大的鼓勵,謝謝你的支持!
下期是 reentrantlock
源碼文章,這個是 Java 層面的,應(yīng)該還好,哈哈哈哈
我是愛敲代碼的小黃,獨(dú)角獸企業(yè)的Java開發(fā)工程師,CSDN博客專家,Java領(lǐng)域新星創(chuàng)作者,喜歡后端架構(gòu)和中間件源碼。文章來源:http://www.zghlxwxcb.cn/news/detail-422852.html
我們下期再見。文章來源地址http://www.zghlxwxcb.cn/news/detail-422852.html
到了這里,關(guān)于《吊打面試官系列》從源碼全面解析 ThreadLocal 關(guān)鍵字的來龍去脈的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!