您好,登錄后才能下訂單哦!
深入理解ThreadLocal
用途
我們一般用ThreadLocal來提供線程局部變量。線程局部變量會在每個Thread內擁有一個副本,Thread只能訪問自己的那個副本。文字解釋總是晦澀的,我們來看個例子。
public class Test {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new MyThread("lucy");
Thread thread2 = new MyThread("lily");
thread1.start();
thread2.start();
}
private static class MyThread extends Thread {
MyThread(String name) {
super(name);
}
@Override
public void run() {
Thread thread = Thread.currentThread();
threadLocal.set("i am " + thread.getName());
try {
//睡眠兩秒,確保線程lucy和線程lily都調用了threadLocal的set方法。
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName() + " say: " + threadLocal.get());
}
}
}
這個例子非常簡單,就是創建了lucy和lily兩個線程。在線程內部,調用threadLocal的set方法存入一字符串,睡眠2秒后輸出線程名稱和threadLocal中的字符串。我們運行這單代碼,看一下輸出內容。
lucy say: i am lucy
lily say: i am lily
原理
上面例子很好的解釋了ThreadLocal的作用,接下來我們分析一下這是如何實現的。
我們定位到ThreadLocal的set方法。源碼中set方法被拆分為幾個方法,為了表述方便筆者將這幾個方法進行了整合。
public void set(T value) {
//獲取當前線程
Thread t = Thread.currentThread();
//獲取當前線程的ThreadLocalMap
ThreadLocalMap map = t.threadLocals;
if (map != null)
//將數據放入ThreadLocalMap中,key是當前ThreadLocal對象,值是我們傳入的value。
map.set(this, value);
else
//初始化ThreadLocalMap,并以當前ThreadLocal對象為Key,value為值存入map中。
t.threadLocals = new ThreadLocalMap(this, value);
}
通過上面這段代碼可以看到,ThreadLocal的set方法主要是通過當前線程的ThreadLocalMap實現的。ThreadLocalMap是一個Map,它的key是ThreadLoacl,value是Object。
TreadLocal的get方法的源碼我就不貼出來了,大體上與set方法類似,就是先獲取到當前線程的ThreadLocalMap,然后以this為key可以取得value。
到這里我們基本上明白了ThreadLocal的工作原理,我們總結一下
每個Thread實例內部都有一個ThreadLocalMap,ThreadLocalMap是一種Map,它的key是ThreadLocal,value是Object。
ThreadLocal的set方法其實是往當前線程的ThreadLocalMap中存入數據,其key是當前ThreadLocal對象,value是set方法中傳入的值。
使用數據時,以當前ThreadLocal為key,從當前線程的ThreadLocalMap中取出數據。
ThreadLocalMap
上面我們介紹了ThreadLocal主要是通過線程的ThreadLocalMap實現的。
static class ThreadLocalMap {
private ThreadLocal.ThreadLocalMap.Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> var1, Object var2) {
super(var1);
this.value = var2;
}
}
}
ThreadLocalMap是一種Map,其內部維護著一個Entry[]。
ThreadLocalMap其實是就是將Key和Value包裝成Entry,然后放入Entry數組中。我們看一下它的set方法。
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) {
//如果已經存在,直接替換value
e.value = value;
return;
}
if (k == null) {//如果當前位置的key ThreadLocal為空,替換key和value。下文ThreadLocal內存分析中會提到為什么會有這段代碼。
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);//該位置沒有數據,直接存入。
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) //檢查是否擴容
rehash();
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
到這里,如果你了解HashMap,應該可以看出ThreadLocalMap就是一種HashMap。不過它并沒有采用java.util.HashMap中數組+鏈表的方式解決Hash沖突,而是采用index后移的方式。
我們簡單分析一下這段代碼:
通過ThreadLocal的threadLocalHashCode與當前Map的長度計算出數組下標 i。
從i開始遍歷Entry數組,這會有三種情況:
Entry的key就是我們要set的ThreadLocal,直接替換Entry中的value。
Entry的key為空,直接替換key和value。
發生了Hash沖突,當前位置已經有了數據,查找下一個可用空間。
找到沒有數據的位置,將key和value放入。
檢查是否擴容。
我們知道,HashMap是一種get、set都非常高效的集合,它的時間復雜度只有O(1)。但是如果存在嚴重的Hash沖突,那HashMap的效率就會降低很多。我們通過上段代碼知道,ThreadLocalMap是通過 key.threadLocalHashCode & (len-1)計算Entry存放index的。len是當前Entry[]的長度,這沒什么好說的。那看來秘密就在threadLocalHashCode中了。我們來看一下threadLocalHashCode是如何產生的。
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
這段代碼非常簡單。有個全局的計數器nextHashCode,每有一個ThreadLocal產生這個計數器就會加0x61c88647,然后把當前值賦給threadLocalHashCode。
ThreadLocal內存分析
不知從何時起,網上開始流傳ThreadLocal有內存泄漏的問題。下面我們從ThreadLocal的內存入手,分析一下這種說法是否正確。話不多說直接上圖。
現在,我們假設ThreadLocal完成了自己的使命,與ThreadLocalRef斷開了引用關系。此時內存圖變成了這樣。
系統GC發生時,由于Heap中的ThreadLocal只有來自key的弱引用,因此ThreadLocal內存會被回收到。
到這里,value被留在了Heap中,而我們沒辦法通過引用訪問它。value這塊內存將會持續到線程結束。如果不想依賴線程的生命周期,那就調用remove方法來釋放value的內存吧。個人認為,這種設計應該也是JDK開發大佬的無奈之舉。我們從源碼中來感受一下這些大佬為了盡可能降低內存泄漏風險作出的努力。
ThreadLocalMap.Entry軟引用ThreadLocal,避免了ThreadLocal的內存泄漏。
還記得ThreadLocalMap set方法中的這段代碼嗎?
private void set(ThreadLocal<?> key, Object value) {
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
...
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
}
ThreadLocal get方法獲取時,有一段如果Entry的key為null,移除Entry和Entry.value的代碼。
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--;
}
...
}
return i;
}
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。