我們通常用ThreadLocal來實現線程局部變量的存儲。在許多開源框架中ThreadLocal被廣泛使用。這篇文章來討論一下ThreadLocal的實現原理
一、ThreadLocal的實現原理
ThreadLocal的實現原理其實很簡單,我們先來看一下ThreadLocal常用的幾個方法:
public void set(T value)
public void remove()
public T get()
通過set、get、remove這個三個方法,可以實現線程局部變量的添加、獲取、刪除。
首先我們先來看一下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);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
我們可以看到set方法是將對象value保存在當前線程的threadLocals這個ThreadLocalMap中,以當前的ThreadLocal對象作為map的鍵值。ThreadLoccaMap是ThreadLocal類的一個內部類,我們先不管它的實現,先來看一下ThreadLocal的get方法和remove方法的實現。
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();
}
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;
}
我們可以看到,當當前線程的ThreadLocalMap對象存在的時候,返回值從這個對象中獲取,這個和set方法保存value是相對應的,都是從當前線程保存的ThreadLocalMap對象中存儲和獲取。當ThreadLocalMap對象不存在的時候返回 setInitialValue() 返回的對象。這個方法我們可以看到:通過 initialValue()方法獲得一個value對象,這個方法默認返回一個null,留給子類實現,用來初始化一個用來保存的對象的默認值。然后將這個對象放在當前線程的ThreadLocalMap中(如果不存在ThreadLocalMap對象就創(chuàng)建一個)。
remove方法:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
依然是調用ThreadLocalMap的remove方法。
好了,究竟ThreadLocalMap是什么?我們接下來看一下:
其實ThreadLocalMap是一個Map,它的實現和HashMap類似,我們先來看一下用來保存K-V的節(jié)點:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
我們先不用管WeakReference是什么,我們只需要知道,Entry 保存了k和v。
接下來看一下ThreadLocalMap的set方法的實現:
private Entry[] table;
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();
}
ThreadLocalMap的set方法和早期HashMap的實現類似,都是先計算哈希,然后確定hash槽的位置,不同的是,ThreadLocalMap通過數組存儲K-V對象(Entry),而HashMap是通過散列表存儲K-V對象。ThreadLocalMap首先獲得存儲數組的長度,然后通過hash算法計算要設置的節(jié)點所在的哈希槽的位置,如果哈希槽的位置沒有元素,就新創(chuàng)建一個Entry對象放在這里。如果有元素,就判斷該元素的k是否和當前要設置的k相等,如果是就將這個哈希槽存儲的entry對象的value重新賦值;如果k是空的話,說明這個ThreadLocal對象被手動設置為null了,是無效的。就把這個節(jié)點替換掉,具體怎么實現看一下replaceStaleEntry這個方法,這里不贅述。如果當前哈希槽位置有合法元素,并且k不和要保存的k相等,就去下一個哈希槽的位置重復檢查,下一個哈希槽的位置是這個方法計算的:
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
添加完元素之后,會去判斷當前存儲數組內元素的數量是否超過了threshold,我們可以叫threshold為擴容因子,threshold = len * 2 / 3。當超過擴容因子的時候就去檢查并且移除壞節(jié)點。移除壞節(jié)后,如果size >= threshold - threshold / 4,就要真正的擴容。我們看一下這個方法:
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
expungeStaleEntries()這個方法,從方法名上可以看出這個方法的作用:去除壞掉的Entries。什么是壞掉的Entries呢?我們可以看一下這個方法的實現:
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
當一個節(jié)點不是null時,調用節(jié)點的get()方法,如果說得的結果是null,這個節(jié)點就是壞的節(jié)點。Entry的get方法其實是他父類WeakReference<T>的父類Reference<T>的方法。這兩個類是什么呢?對JVM有了解的小伙伴應該對這兩個類不陌生,我們知道,在java中,對象的引用分為四種:強引用、軟引用、弱引用、虛引用。引用強度逐漸減弱。
強引用是我們常見的對象引用,比如:Object o = new Object();
只要一個對象被強引用所引用,就不會被垃圾收集器回收。當內存不足時,jvm會拋出OOM異常。
軟引用對應著Reference<T>的實現類SoftReference<T>,這種引用引用的對象不會立刻被回收,但是當內存空間不足的時候,垃圾收集器就會回收軟引用所引用的對象。
弱引用對應著Reference<T>的實現類WeakReference<T>,當軟引用指向的對象被垃圾收集器發(fā)現后,就會回收這個對象(只有軟引用引用這個對象,如果強引用同時也引用這個對象時,這個對象并不會被回收)。
虛引用:也叫幽靈引用,虛引用主要用來跟蹤對象被垃圾回收器回收的活動。不會影響任何垃圾回收的過程。
回到前面的所說的判斷是否為壞的節(jié)點,e.get()所獲得的其實是e存儲的key,也就是ThreadLocal對象。所以我們可以看出,Thread中的ThreadLocalMap并不會影響ThreadLocal在jvm中的生命周期。當一個節(jié)點被判定為壞節(jié)點后,這個節(jié)點就會被移除,具體實現我們看一下expungeStaleEntry這個方法:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
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 {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
首先將位置為staleSlot處存儲的Entry的對象的value也設置為null,然后將這個對象從存儲的數組中移除,并將size減1。然后一次檢查下一個位置(nextIndex(staleSlot, len)確定下一個位置)是否為壞節(jié)點,是的話就移除,否則重新計算這個節(jié)點所在的位置,將這個節(jié)點移動到計算后的新位置。這樣做的原因是因為在set節(jié)點的時候,如果存在hash沖突,并且key不相等時,會將調用nextIndex(staleSlot, len)方法重新確定hash槽的位置。
真正的擴容方法是由resize()方法實現的,實現過程是很簡單。我們看一下具體實現:
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
我們可以看到,每次擴容大小都是原來大小的2倍,擴容的過程就是新建一個大小為原來2倍的數組,將原來數組內的元素放到新數據中。過程很簡單這里就不在贅述。
二、疑問,線程池中,大量任務使用ThreadLocal會不會造成OOM
根據上面的分析,ThreadLocal實現線程局部存儲是通過每個線程Thread中的ThreadLocalMap存儲以ThreadLocal對象為key,以要存儲的對象為value來實現的。而ThreadLocalMap中,存儲K-V是通過Entry實現,Entry繼承了WeakReference。所以ThreadLocalMap不會影響ThreadLocal對象的在內存中的回收。通過之前《java線程池淺析》這篇我們可以知道線程池實現的原理其實是多個(或一個)線程執(zhí)行提交的runnable任務。runnable任務中使用ThreadLocal,當runnable任務結束(其實是run方法結束),runnable任務中使用的ThreadLocal就會失去和GCroot的連接,這個時候只有ThreadLocalMap中的Entry會引用該ThreadLocal對像,所以當內存不足的時候,ThreadLocal對像會被回收。所以在線程池中,這種ThreadLocal的使用是不會造成OOM的。