鵝廠一面,有關(guān) ThreadLocal 的一切
1. 底層結(jié)構(gòu)
ThreadLocal 底層有一個(gè)默認(rèn)容量為 16 的數(shù)組組成,k 是 ThreadLocal 對(duì)象的引用,v 是要放到 TheadLocal 的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
數(shù)組類似為 HashMap,對(duì)哈希沖突的處理不是用鏈表/紅黑樹處理,而是使用鏈地址法,即嘗試順序放到哈希沖突下標(biāo)的下一個(gè)下標(biāo)位置。
該數(shù)組也可以進(jìn)行擴(kuò)容。
2. 工作原理
一個(gè) ThreadLocal 對(duì)象維護(hù)一個(gè) ThreadLocalMap 內(nèi)部類對(duì)象,ThreadLocalMap 對(duì)象才是存儲(chǔ)鍵值的地方。
更準(zhǔn)確的說,是 ThreadLocalMap 的 Entry 內(nèi)部類是存儲(chǔ)鍵值的地方
見源碼 set(),createMap() 可知。
因?yàn)橐粋€(gè) Thread 對(duì)象維護(hù)了一個(gè) ThreadLocal.ThreadLocalMap 成員變量,且 ThreadLocal 設(shè)置值時(shí),獲取的 ThreadLocalMap 正是當(dāng)前線程對(duì)象的 ThreadLocalMap。
// 獲取 ThreadLocalMap 源碼
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
所以每個(gè)線程對(duì) ThreadLocal 的操作互不干擾,即 ThreadLocal 能實(shí)現(xiàn)線程隔離
3. 使用
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("七淅在學(xué)Java");
Integer i = threadLocal.get()
// i = 七淅在學(xué)Java
4. 為什么 ThreadLocal.ThreadLocalMap 底層是長(zhǎng)度 16 的數(shù)組呢?
對(duì) ThreadLocal 的操作見第 3 點(diǎn),可以看到 ThreadLocal 每次 set 方法都是對(duì)同個(gè) key(因?yàn)槭峭瑐€(gè) ThreadLocal 對(duì)象,所以 key 肯定都是一樣的)進(jìn)行操作。
如此操作,看似對(duì) ThreadLocal 的操作永遠(yuǎn)只會(huì)存 1 個(gè)值,那用長(zhǎng)度為 1 的數(shù)組它不香嗎?為什么還要用 16 長(zhǎng)度呢?
好了,其實(shí)這里有個(gè)需要注意的地方,ThreadLocal 是可以存多個(gè)值的
那怎么存多個(gè)值呢?看如下代碼:
// 在主線程執(zhí)行以下代碼:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("七淅在學(xué)Java");
ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
threadLocal2.set("七淅在學(xué)Java2");
按代碼執(zhí)行后,看著是 new 了 2 個(gè) ThreadLocal 對(duì)象,但實(shí)際上,數(shù)據(jù)的存儲(chǔ)都是在同一個(gè) ThreadLocal.ThreadLocalMap 上操作的
再次強(qiáng)調(diào):ThreadLocal.ThreadLocalMap 才是數(shù)據(jù)存取的地方,ThreadLocal 只是 api 調(diào)用入口)。真相在 ThreadLocal 類源碼的 getMap()
因此上述代碼最終結(jié)果就是一個(gè) ThreadLocalMap 存了 2 個(gè)不同 ThreadLocal 對(duì)象作為 key,對(duì)應(yīng) value 為 七淅在學(xué)Java、七淅在學(xué)Java2。
我們?cè)倏聪?ThreadLocal 的 set 方法
public void set(T value) {
Thread t = Thread.currentThread();
// 這里每次 set 之前,都會(huì)調(diào)用 getMap(t) 方法,t 是當(dāng)前調(diào)用 set 方法的線程
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 重點(diǎn):返回調(diào)用 set 方法的線程(例子是主線程)的 ThreadLocal 對(duì)象。
// 所以不管 api 調(diào)用方 new 多少個(gè) ThreadLocal 對(duì)象,它永遠(yuǎn)都是返回調(diào)用線程(例子是主線程)的 ThreadLocal.ThreadLocalMap 對(duì)象供調(diào)用線程去存取數(shù)據(jù)。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// t.threadLocals 的聲明如下
ThreadLocal.ThreadLocalMap threadLocals = null;
// 僅有一個(gè)構(gòu)造方法
public ThreadLocal() {
}
5. 數(shù)據(jù)存放在數(shù)組中,那如何解決 hash 沖突問題
使用鏈地址法解決。
具體怎么解決呢?看看執(zhí)行 get、set 方法的時(shí)候:
set: 且數(shù)組的 key 等于該 ThreadLocal,則覆蓋該位置元素 否則就找下一個(gè)空位置,直到找到空或者 key 相等為止。 根據(jù) ThreadLocal 對(duì)象的 hash 值,定位到 ThreadLocalMap 數(shù)組中的位置。 如果位置無元素則直接放到該位置 如果有元素 get: 根據(jù) ThreadLocal 對(duì)象的 hash 值,定位到 ThreadLocalMap 數(shù)組中的位置。 如果不一致,就判斷下一個(gè)位置 否則則直接取出
// 數(shù)組元素結(jié)構(gòu)
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
6. ThreadLocal 的內(nèi)存泄露隱患
三個(gè)前置知識(shí):
ThreadLocal 對(duì)象維護(hù)一個(gè) ThreadLocalMap 內(nèi)部類 ThreadLocalMap 對(duì)象又維護(hù)一個(gè) Entry 內(nèi)部類,并且該類繼承弱引用 WeakReference<ThreadLocal<?>>,用來存放作為 key 的 ThreadLocal 對(duì)象(可見最下方的 Entry 構(gòu)造方法源碼),可見最后的源碼部分。不管當(dāng)前內(nèi)存空間足夠與否,GC 時(shí) JVM 會(huì)回收弱引用的內(nèi)存
因?yàn)?ThreadLocal 作為弱引用被 Entry 中的 Key 變量引用,所以如果 ThreadLocal 沒有外部強(qiáng)引用來引用它,那么 ThreadLocal 會(huì)在下次 JVM 垃圾收集時(shí)被回收。
這個(gè)時(shí)候 Entry 中的 key 已經(jīng)被回收,但 value 因?yàn)槭菑?qiáng)引用,所以不會(huì)被垃圾收集器回收。這樣 ThreadLocal 的線程如果一直持續(xù)運(yùn)行,value 就一直得不到回收,導(dǎo)致發(fā)生內(nèi)存泄露。
如果想要避免內(nèi)存泄漏,可以使用 ThreadLocal 對(duì)象的 remove() 方法
7. 為什么 ThreadLocalMap 的 key 是弱引用
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
為什么要這樣設(shè)計(jì),這樣分為兩種情況來討論:
key 使用強(qiáng)引用:只有創(chuàng)建 ThreadLocal 的線程還在運(yùn)行,那么 ThreadLocalMap 的鍵值就都會(huì)內(nèi)存泄漏,因?yàn)?ThreadLocalMap 的生命周期同創(chuàng)建它的 Thread 對(duì)象。 key 使用弱引用:是一種挽救措施,起碼弱引用的值可以被及時(shí) GC,減輕內(nèi)存泄漏。另外,即使沒有手動(dòng)刪除,作為鍵的 ThreadLocal 也會(huì)被回收。因?yàn)?ThreadLocalMap 調(diào)用 set、get、remove 時(shí),都會(huì)先判斷之前該 value 對(duì)應(yīng)的 key 是否和當(dāng)前調(diào)用的 key 相等。如果不相等,說明之前的 key 已經(jīng)被回收了,此時(shí) value 也會(huì)被回收。因此 key 使用弱引用是最優(yōu)的解決方案。
8. 父子線程如何共享 ThreadLocal 數(shù)據(jù)
主線程創(chuàng)建 InheritableThreadLocal 對(duì)象時(shí),會(huì)為 t.inheritableThreadLocals 變量創(chuàng)建 ThreadLocalMap,使其初始化。其中 t 是當(dāng)前線程,即主線程 創(chuàng)建子線程時(shí),在 Thread 的構(gòu)造方法,會(huì)檢查其父線程的 inheritableThreadLocals 是否為 null。從第 1 步可知不為 null,接著 將父線程的 inheritableThreadLocals 變量值復(fù)制給這個(gè)子線程。 InheritableThreadLocal 重寫了 getMap, createMap, 使用的都是 Thread.inheritableThreadLocals 變量
如下:
public class InheritableThreadLocal<T> extends ThreadLocal<T>
第 1 步:對(duì) InheritableThreadLocal 初始化
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
第 2 步:創(chuàng)建子線程時(shí),判斷父線程的 inheritableThreadLocals 是否為空。非空進(jìn)行復(fù)制
// Thread 構(gòu)造方法中,一定會(huì)執(zhí)行下面邏輯
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
第 3 步:使用對(duì)象為第 1 步創(chuàng)建的 inheritableThreadLocals 對(duì)象
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
}
// 示例:
// 結(jié)果:能夠輸出「父線程-七淅在學(xué)Java」
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("父線程-七淅在學(xué)Java");
Thread t = new Thread(() -> System.out.println(threadLocal.get()));
t.start();
// 結(jié)果:null,不能夠輸出「子線程-七淅在學(xué)Java」
ThreadLocal threadLocal2 = new InheritableThreadLocal();
Thread t2 = new Thread(() -> {
threadLocal2.set("子線程-七淅在學(xué)Java");
});
t2.start();
System.out.println(threadLocal2.get());

往期推薦

面試突擊55:delete、drop、truncate有什么區(qū)別?

面渣逆襲:MyBatis連環(huán)20問,這誰頂?shù)米。?/p>

面渣逆襲:RocketMQ二十三問

