ThreadLocal原始碼解析,記憶體洩露以及傳遞性
我想ThreadLocal這東西,大家或多或少都瞭解過一點,我在接觸ThreadLocal的時候,覺得這東西很神奇,在網上看了很多部落格,也看了一些書,總覺得有一個坎跨不過去,所以對ThreadLocal一直是一知半解的,好在這東西在實際開發中畢竟用的不多,所以也就得過且過了。當然我說的“用的不多”,只是對於普通的上層業務開發而言,其實在很多框架中,都用到了ThreadLocal,甚至有的還對ThreadLocal做了進一步的改進。但是ThreadLocal也算是併發程式設計的基礎,所以還真的有必要,也必須要好好研究下的。今天我們就來好好看看ThreadLocal。
ThreadLocal簡單應用
我們知道在多執行緒下,操作一個共享變數,很容易會發生矛盾,要解決這問題,最好的辦法當然是每個執行緒都擁有自己的變數,其他的執行緒無法訪問,所謂“沒有共享,就沒有傷害”。那麼如何做到呢?ThreadLocal就這樣華麗麗的登場了。
我們先來看看簡單的應用:
public static void main(String[] args) { ThreadLocal threadLocal = new ThreadLocal(); threadLocal.set("Hello"); System.out.println("當前執行緒是:" + Thread.currentThread().getName()); System.out.println("在當前執行緒中獲取:" + threadLocal.get()); new Thread(() -> System.out.println("現線上程是"+Thread.currentThread().getName()+"嘗試獲取:" + threadLocal.get())).start(); }
執行結果:
當前執行緒是:main
在當前執行緒中獲取:Hello
現線上程是Thread-0嘗試獲取:null
執行結果很好理解,在主執行緒中往threadLocal 塞了一個值,只有在同一個執行緒下,才可以獲得值,在其他執行緒就無法獲取值了。
嘗試自己寫一個ThreadLocal
在我們探究ThreadLocal之前,先讓我們思考一個問題,如果叫你來實現ThreadLocal,你會怎麼做?
ThreadLocal的目標就在於讓每個執行緒都有隻屬於自己的變數。最直接的辦法就是新建一個泛型類,在類中定義一個map,key是Long型別的,用來儲存執行緒的id,value是T型別的,用來儲存具體的資料。
set的時候,就獲取當前執行緒的id,把這個作為key,往map裡面塞資料;
get的時候,還是獲取當前執行緒的id,把這個作為key,然後從map中取出資料。
就像下面這個樣子:
public class ThreadLocalTest {
public static void main(String[] args) {
CodeBearThreadLocal threadLocal = new CodeBearThreadLocal();
threadLocal.set("Hello");
System.out.println("當前執行緒是:" + Thread.currentThread().getName());
System.out.println("在當前執行緒中獲取:" + threadLocal.get());
new Thread(() -> System.out.println("現線上程是" + Thread.currentThread().getName() + "嘗試獲取:" + threadLocal.get())).start();
}
}
class CodeBearThreadLocal<T> {
private ConcurrentHashMap<Long , T> hashMap = new ConcurrentHashMap<>();
void set(T value) {
hashMap.put(Thread.currentThread().getId(),value);
}
T get() {
return hashMap.get(Thread.currentThread().getId());
}
}
執行結果:
當前執行緒是:main
在當前執行緒中獲取:Hello
現線上程是Thread-0嘗試獲取:null
可以看到執行結果和“正版的ThreadLocal”是一模一樣的。
探究ThreadLocal
我們自己也寫了一個ThreadLocal,看上去一點問題也沒有,僅僅幾行程式碼就把功能實現了,給自己鼓個掌。那正版的ThreadLocal是怎麼實現的呢?核心應該和我們寫的差不多吧。遺憾的是,正版的ThreadLocal和我們寫的可以說完全不一樣。
我們現在看看正版的ThreadLocal是怎麼做的。
set
public void set(T value) {
Thread t = Thread.currentThread();//獲取當前的執行緒
ThreadLocalMap map = getMap(t);//獲取ThreadLocalMap
if (map != null)//如果map不為null,呼叫set方法塞入值
map.set(this, value);
else
createMap(t, value);//新建map
}
- 獲取當前的執行緒賦值給t;
- 呼叫getMap方法,傳入t,也就是傳入當前執行緒,獲取ThreadLocalMap,賦值給map;
- 如果map不為null,呼叫set方法塞入值;
- 如果map為null,則呼叫createMap方法。
讓我們來看看getMap方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
getMap方法比較簡單,直接返回了傳進來的執行緒物件的threadLocals,說明threadLocals定義在Thread類裡面,是ThreadLocalMap 型別的,讓我們看看threadLocals的定義:
public class Thread implements Runnable{
ThreadLocal.ThreadLocalMap threadLocals = null;
}
看到這個定義,大家一定有點暈,我們是跟著ThreadLocal的set方法進來的,怎麼到了這裡又回到ThreadLocal了,大家彆著急,我們再來看看ThreadLocalMap是什麼鬼?
ThreadLocalMap是ThreadLocal的靜態內部類,我們的資料就是儲存在ThreadLocalMap裡面,更詳細的說我們的資料就儲存在ThreadLocal類中的ThreadLocalMap靜態內部類中的Entry[]裡面。
讓我們把關係理一理,確實有點混亂,Thread類裡面定義了ThreadLocal.ThreadLocalMap欄位,ThreadLocalMap是TheadLocal的內部靜態類,其中的Entry[]是用來儲存資料的。這就意味著,每一個Thread例項中的ThreadLocalMap都是獨一無二的,又不相互干擾。等等,這不就揭開了ThreadLocal的神祕面紗了嗎?原來ThreadLocal是這麼做到讓每個執行緒都有自己的變數的。
如果你還不清楚的話,沒關係,我們再來說的詳細點。在我們實現的ThreadLocal中,是利用map實現資料儲存的,key就是執行緒Id,你可以理解為key就是Thread的例項,value就是我們需要儲存的資料,當我們呼叫get方法的時候,就是利用執行緒Id,你可以理解為利用Thread的例項去map中取出資料,這樣我們取出的資料就肯定是這個執行緒持有的。比如這個執行緒是A,你傳入了B執行緒的執行緒Id,也就是傳入了B執行緒的Thread的例項就肯定無法取出執行緒A所持有的資料,這點應該毫無疑問把。但是,在正版的ThreadLocal中,資料是直接存在Thread例項中的,這樣每個執行緒的資料就被天然的隔離了。
現在我們解決了一個問題,ThreadLocal是如何實現執行緒資料隔離的,但是還有一個問題,也就是我初學ThreadLocal看了很多部落格,仍然百思不得其解的問題,既然資料是儲存在ThreadLocalMap中的Entry[]的,那麼就代表可以儲存多個數據,不然用一個普通的成員變數不就OK了嗎,為什麼要用陣列呢?但是ThreadLocal提供的set方法沒有過載啊,如果先set一個“hello”,又set一個“bye”,那麼“bye”肯定會把“hello”給覆蓋掉啊,又不像HashMap一樣,有key和value的概念。這個問題真的困擾我很久,後面終於知道了原因了,我們可以new多個ThreadLocal呀,就像這樣:
public static void main(String[] args) {
ThreadLocal threadLocal1 = new ThreadLocal();
threadLocal1.set("Hello");
ThreadLocal threadLocal2 = new ThreadLocal();
threadLocal2.set("Bye");
}
這樣一來,會發生什麼情況呢?再次放出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);
}
threadLocal1,threadLocal2都呼叫了set方法,儘管threadLocal1和threadLocal2是不同的例項,但是它們在同一個執行緒啊,所以getMap獲取的ThreadLocalMap是同一個,這樣就變成了在同一個ThreadLocalMap儲存了多個數據。
具體是怎麼儲存資料的,這個程式碼就比較複雜了,包括的細節太多了,我看的也不是很懂,只知道一個大概,我們先來看看Entry的定義把:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
Entry又是ThreadLocalMap的靜態內部類,裡面只有一個欄位value,也就是說和HashMap是不同的,沒有連結串列的概念。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
- 把table賦值給區域性變數tab,這個table就是儲存資料的欄位,型別是Entry[];
- 獲取tab的長度賦值給len;
- 求出下標i;
- 一個for迴圈,先根據第三步求出的下標,從tab裡獲取指定下標的值e,如果e==null,就不會進入這個for迴圈,也就是如果當前的位置是空的,就直接進入第五步;如果當前的位置已經有資料了,判斷這個位置的ThreadLocal和我們即將要插入進去的是不是同一個,如果是的話,用新值替換掉;如果不是的話,則尋找下一個空位;
- 把創建出來的Entry例項放入tab。
其中的細節有點多,看的有點迷糊,但是最關鍵的應該還算是看懂了。
get
public T get() {
Thread t = Thread.currentThread();//獲取當前執行緒
ThreadLocalMap map = getMap(t);//傳入當前執行緒,獲取當前執行緒的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//傳入ThreadLocal例項,獲取Entry
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;//返回值
}
}
return setInitialValue();
}
- 獲取當前執行緒;
- 獲取當前執行緒的ThreadLocalMap;
- 傳入ThreadLocal例項,獲取Enrty;
- 返回值。
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
- 求出下標i;
- 根據下標i,從table中取出值,賦值給e;
- 如果e不為空,並且e持有的ThreadLocal例項和傳進去的ThreadLocal例項是同一個,直接返回;
- 如果e為空,或者e持有的ThreadLocal例項和傳進去的ThreadLocal例項不是同一個,則繼續往下找。
小總結
set方法和get方法都分析完畢了,我們來做一個小總結。我們在外面所使用的ThreadLocal更像是一個工具類,本身不儲存任何資料,而真正的資料是儲存在Thread例項中的,這樣就天然的完成了執行緒資料的隔離。最後送上一張圖,來幫助大家更好的理解ThreadLocal:
記憶體洩露
我們再來看看Entry的定義:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry繼承了WeakReference,關於WeakReference是什麼東西,不是本文的重點,大家可以自行查閱。WeakReference包裹了ThreadLocal,我們再來看Entry的構造方法,呼叫了super(k),傳入了我們傳進來的ThreadLocal例項,也就是ThreadLocal被儲存到了WeakReference物件中。這就導致了一個問題,當ThreadLocal沒有強依賴,ThreadLocal會在下一次發生GC時被回收,key是被回收了,但是value卻沒有被回收呀,所以就出現了Entry[]存在key為NULL,但是value不為NULL的項的情況,要想回收的話,可以讓建立ThreadLocal的執行緒的生命週期結束。但是在實際的開發中,執行緒有極大可能是和程式同生共死的,只要程式不停止,執行緒就一直在蹦躂。所以我們在使用完ThreadLocal方法後,最好要手動呼叫remove方法,就像這樣:
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal();
try {
threadLocal.set("Hello");
threadLocal.get();
} finally {
threadLocal.remove();
}
}
別忘了,最好把remove方法放在finally中哦。
InheritableThreadLocal
我們還是來看部落格一開頭的例子:
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("Hello");
System.out.println("當前執行緒是:" + Thread.currentThread().getName());
System.out.println("在當前執行緒中獲取:" + threadLocal.get());
new Thread(() -> System.out.println("現線上程是" + Thread.currentThread().getName() + "嘗試獲取:" + threadLocal.get())).start();
}
執行結果:
當前執行緒是:main
在當前執行緒中獲取:Hello
現線上程是Thread-0嘗試獲取:null
程式碼後面new出來Thread是由主執行緒建立的,所以可以說這個執行緒是主執行緒的子執行緒,在主執行緒往ThreadLocal set的值,在子執行緒中獲取不到,這很好理解,因為他們並不是同一個執行緒,但是我希望子執行緒能繼承主執行緒的ThreadLocal中的資料。InheritableThreadLocal出現了,完全可以滿足這樣的需求:
public static void main(String[] args) {
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("Hello");
System.out.println("當前執行緒是:" + Thread.currentThread().getName());
System.out.println("在當前執行緒中獲取:" + threadLocal.get());
new Thread(() -> System.out.println("現線上程是" + Thread.currentThread().getName() + "嘗試獲取:" + threadLocal.get())).start();
}
執行結果:
當前執行緒是:main
在當前執行緒中獲取:Hello
現線上程是Thread-0嘗試獲取:null
這樣就讓子執行緒繼承了主執行緒的ThreadLocal的資料,說的更準確些,是子執行緒繼承了父執行緒的ThreadLocal的資料。
那到底是如何做到的呢?還是看程式碼把。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
InheritableThreadLocal繼承了ThreadLocal,並且重寫了三個方法,當我們首次呼叫InheritableThreadLocal的set的時候,會呼叫InheritableThreadLocal的createMap方法,這就建立了ThreadLocalMap的例項,並且賦值給inheritableThreadLocals,這個inheritableThreadLocals定義在哪裡呢?和ThreadLocal的threadLocals一樣,也是定義在Thread類中。當我們再次呼叫set方法的時候,會呼叫InheritableThreadLocal的getMap方法,返回的也是inheritableThreadLocals,也就是把原先的threadLocals給替換掉了。
當我們建立一個執行緒,會呼叫Thread的構造方法:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
init方法比較長,我只複製出和我們要探究的問題相關的程式碼:
Thread parent = currentThread();
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
- 獲取當前執行緒,此時當前執行緒是父執行緒。
- 如果父執行緒的inheritableThreadLocals不為空,就跑到if中去。當然這裡肯定是不為空的,我們上面已經說了,呼叫InheritableThreadLocal中的set方法,直接操作的是inheritableThreadLocals,if中做了什麼,就是傳入了父執行緒的inheritableThreadLocals,建立了新的ThreadLocalMap,賦值給Thead例項的inheritableThreadLocals,這樣子執行緒就擁有了父執行緒的ThreadLocalMap,也就完成了ThreadLocal的繼承與傳遞。
這篇部落格到這裡就結束了,東西還是挺多的,但是都是挺重要的,特別是ThreadLocal的原因和產生記憶體洩露的原因和避免的方法