浪尖整理本文主要是想幫助大家完全消化面試中常見的ThreadLocal問題。希望讀懂此文以後大家可以掌握(沒耐心的可以直接閱讀底部總結):
- 簡單介紹原理
- ThreadLocal使用案例場景
- Threadlocal的底層原理
- Threadlocal內存溢出原因
- 解決方法
1. 簡介
高併發處理起來比較麻煩,很多新手對此都會非常頭疼。要知道避免併發的最簡單辦法就是線程封閉,也即是把對象封裝到一個線程裏,那麼對象就只會被當前線程能看到,使得對象就算不是線程安全的也不會出現任何安全問題。Threadlocal是實現該策略的最好的方法。Threadlocal爲每個線程提供了一個私有變量,然後線程訪問該變量(get或者set)的時候實際上是讀寫的自己的局部變量從而避免了併發法問題。
2. 案例使用
首先定義一個ThreadLocal的封裝工具類
public class Bank {
/*ThreadLocal<Integer> t = ThreadLocal.withInitial(() -> 100);*/
ThreadLocal<Integer> t = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 100;
}
};
public int get() {
return t.get();
}
public void set() {
t.set(t.get() + 10);
}
}
實現一個Runnable對象然後使用bank對象
public class Transfer implements Runnable {
private Bank bank;
public Transfer(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
bank.set();
System.out.println(Thread.currentThread() + " : " + bank.get());
}
}
}
定義兩個線程t1和t2,運行之後查看結果:
public class App {
public static void main(String[] args) {
Bank bank = new Bank();
Transfer t = new Transfer(bank);
Thread t1 = new Thread(t);
t1.start();
Thread t2 = new Thread(t);
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(bank.get());
}
}
查看輸出結果就會發現,發現主線程,線程t1,線程t2之間相互不影響~
Thread[Thread-0,5,main] : 110
Thread[Thread-1,5,main] : 110
Thread[Thread-0,5,main] : 120
Thread[Thread-1,5,main] : 120
Thread[Thread-0,5,main] : 130
Thread[Thread-1,5,main] : 130
Thread[Thread-0,5,main] : 140
Thread[Thread-1,5,main] : 140
Thread[Thread-0,5,main] : 150
Thread[Thread-1,5,main] : 150
Thread[Thread-0,5,main] : 160
Thread[Thread-1,5,main] : 160
Thread[Thread-0,5,main] : 170
Thread[Thread-1,5,main] : 170
Thread[Thread-0,5,main] : 180
Thread[Thread-1,5,main] : 180
Thread[Thread-0,5,main] : 190
Thread[Thread-1,5,main] : 190
Thread[Thread-0,5,main] : 200
Thread[Thread-1,5,main] : 200
100
3. 底層源碼
每個線程Thread內部都會有ThreadLocal.ThreadLocalMap對象,該對象是一個自定義的map,key是弱引用包裝的ThreadLocal類型,value就是我們的值。
初始值
Threadlocal直接在構造的時候設置初始值。主要是要實現其initialValue方法:
new ThreadLocal<Integer>() {
@Override
protected IntegerinitialValue() {
return 100;
}
};
追蹤一下該方法,會發現其僅僅被一個私有方法調用了
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
解讀一下setInitialValue私有方法
- 首先調用initialVaule方法,獲取初始值。
- 然後獲取當前線程對象的引用
- 通過線程對象引用獲取ThreadLocal.ThreadLocalMap對象 map。
- map對象不爲空,就將當前threadlocal弱引用作爲key,初始值爲value完成初始化。
- Map對象爲空,調用createMap方法,並完成初始化。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
讀到這可能會很好奇,爲啥只是被私有方法調用,我們又無權調用該私有方法,如何實現初始化呢?也是很簡單的在我們第一次調用get的時候,會調用該私有初始化方法,來真正完成初始化。
Get方法
具體代碼如下:
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();
}
我們來解讀一下get方法,此處就真正暴露ThreadLocal的真實面目了。
- 獲取當前線程對象,t
- 通過getMap(t)方法來獲取t內部的ThreadLocal.ThreadLocalMap對象。
- 然後判斷ThreadLocalMap對象是否爲空,不爲空就可以通過當前Threadlocal對象獲取對應的value值,存在返回,不存在跳過。
- 假如map爲空或者當前threadlocal對象對應的value爲空,那麼就調用初始化方法setInitialValue初始化並返回初始值。
Set方法
接下來解讀一下threadlocal變量的set方法。Set的方法源碼如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
- 獲取當前線程對象 t
- 通過getMap(t)方法來獲取t內部的ThreadLocal.ThreadLocalMap對象 map。
- map不爲空,當前threadlocal對象作爲key(弱引用),要設置的value作爲value完成值的設置。
- 假如map爲空,就調用createMap方法,給當前線程創建一個ThreadlocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
remove方法
threadlocal的remove方法主要作用是刪除當前threadlocal對應的鍵值對。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
4. 內存泄漏
根據前面對threadlocal的整理,其實可以畫出來一個結構圖:
對value的引用線路有兩條:
- threadlocalref 是ThreadLocal強引用,key是ThreadLocal變量的弱引用。由於key是弱引用,當ThreadLocalRef因不用而釋放掉的時候,ThreadLocal對象就會被回收,由於是key到threadLocal對象爲弱引用,一旦進行垃圾回收key就會被回收而相應位置變爲null,當然value依然存在。
- 通過當前線程的引用可以獲取當前線程對象,當前線程對象就可以獲取到ThreadLocalMap,那麼只要當前線程一直存在,ThreadLocalMap對象就會一直存在。
由於ThreadlocalMap存活時間和線程一樣,比如我們採用的是常駐線程池,使用線程過程中沒有清空ThreadLocalMap,也沒有調用threadlocal的remove方法,就將線程放回線程池,雖然ThreadLocal的強引用ThreadLocalRef被清除,弱引用key在GC的時候也會被設置爲null,但是對於value值還存在一條強引用鏈條:
currentThreadRef -> currentThread -> ThreadLocalMap -> Entry(value)
所以value並沒有釋放,就造成了內存泄漏了。
那這時候你或許會問爲啥ThreadLocalMap存儲value的時候不採用弱引用呢?這樣不就可以避免內存泄漏了麼?value是弱引用是不行的,原因很簡單:我們存儲的對象除了ThreadLocalMap的Value就沒有其他的引用了,value一但是對象的弱引用,GC的時候被回收,對象就無法訪問了,這顯然不是我們想要的。
5. 避免內存泄漏
爲避免內存泄漏最好在使用完ThreadLocal之後調用其remove方法,將數據清除掉。
當然,對於Java8 ThreadLocalMap 的 set 方法通過調用 replaceStaleEntry 方法回收鍵爲 null 的 Entry 對象的值(即爲具體實例)以及 Entry 對象本身從而防止內存泄漏
get方法會間接調用expungeStaleEntry 方法將鍵和值爲 null 的 Entry 設置爲 null 從而使得該 Entry 可被回收