ThreadLocal是大家比較常用到的,在多線程下存儲線程相關數據十分合適。可是很多時候我們並沒有深入去了解它的原理。
首選提出幾個問題,稍后再針對這些問題一一解答。
- 提到ThreadLocal,大家常說ThreadLocal是弱引用,那么ThreadLocal究竟是如何實現弱引用的呢?
- ThreadLocal是如何做到可以當做線程局部變量的呢?
- 大家創建ThreadLocal變量時,為什么都要用static修飾?
- 大家爭論不止的ThreadLocal內存泄漏是什么鬼?
進入正題,先簡單了解下ThreadLocal 和 Thread,ThreadLocal的類結構:
可以看到,ThreadLocal有個內部類ThreadLocalMap,ThreadLocalMap又有個內部類Entry。
Thread類有這樣一段源碼:
class Thread implements Runnable { ...省略若干代碼 /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
通過Thread源碼我們了解到,Thread持有的對象是ThreadLocal的ThreadLocalMap,這一點特別重要,線程相關數據都是通過ThreadLocalMap存儲的,而不是ThreadLocal。
此時我們得到的結論如下圖所示:
Thread的threadLocals屬性直接關聯的ThreadLocal.ThreadLocalMap,和ThreadLocal沒有絲毫關系
那么ThreadLocal是做什么的呢?其實ThreadLocal可以看做線程操作ThreadLocalMap的工具類,ThreadLocal暴漏了兩個公共方法get()和set(T)用來獲取和設置ThreadLocalMap。
了解一下set方法源碼:
1 public void set(T value) { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) 5 map.set(this, value); 6 else 7 createMap(t, value); 8 }
從源碼第五行我們可以得到兩個重要的信息:
- 獲取ThreadLocalMap時,使用了當前Thread對象 t 作為參數。
getMap(t)方法的實現很簡單:
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
它返回的是Thread的 threadLocals 屬性,代碼上驗證了:“線程局部變量”是存儲在Thread對象的threadLocals屬性中,和 ThreadLocal 本身沒什么關系。ThreadLocal 可以當做訪問的工具類。
這里我們第2個問題:ThreadLocal是如何做到可以當做線程局部變量的已經有答案啦,所有的操作其實都是對Thread 下 threadLocals 的操作,所以跨線程操作也不會產生問題的,因為getMap()永遠返回當前線程的threadLocals屬性。
- ThreadLocalMap是一個類似Map鍵值對的結構,此處傳入的key是固定值this,這個this不是線程對象喲,是當前的ThreadLocal對象,value即我們傳入的參數。
小伙伴們是不是很奇怪為什么要把this當做key呢?這就扯到我們文章開頭的第一個問題了:弱引用!
跟進map.set(this, value);源碼一看究竟:
1 private void set(ThreadLocal<?> key, Object value) { 2 3 Entry[] tab = table; 4 int len = tab.length; 5 int i = key.threadLocalHashCode & (len-1); 6 7 for (Entry e = tab[i]; 8 e != null; 9 e = tab[i = nextIndex(i, len)]) { 10 ThreadLocal<?> k = e.get(); 11 12 if (k == key) { 13 e.value = value; 14 return; 15 } 16 17 if (k == null) { 18 replaceStaleEntry(key, value, i); 19 return; 20 } 21 } 22 23 tab[i] = new Entry(key, value); 24 int sz = ++size; 25 if (!cleanSomeSlots(i, sz) && sz >= threshold) 26 rehash(); 27 }
查看23行Entry的構造方法:
1 static class Entry extends WeakReference<ThreadLocal<?>> { 2 /** The value associated with this ThreadLocal. */ 3 Object value; 4 5 Entry(ThreadLocal<?> k, Object v) { 6 super(k); 7 value = v; 8 } 9 }
Entry只有一個構造方法,該構造方法接受兩個參數k和v,k就是當前ThreadLocal對象,v是我要存儲的線程相關數據。通過上述代碼標紅部分我們可以了解到對 k 使用了弱引用,但是value不是,value是強引用。至此第一個問題已經真相了,大家所說的ThreadLocal弱引用其實是ThreadLocalMap和ThreadLocal是弱引用關系。
為什么要這么設計呢?
首選我們整理下當前引用關系如下圖:
value一般是線程相關的數據,線程回收后value -> null,強引用就不存在了。但是ThreadLocal對象的生命周期不一定和線程相關,可能線程消亡后ThreadLocal對象仍然被其它線程引用,如果使用強引用的話,ThreadLocalMap對象就無法釋放內存,發生內存泄漏的情況。使用弱引用就安全的多了,發生gc時弱引用指向的對象會被內存回收。
問題1和2已經在上文中提到,繼續看問題3,創建ThreadLocal對象時為什么要用static修飾呢?
個人感覺是基於兩點的考慮:
- 第一是避免重復創建ThreadLocal對象,使用同一個ThreadLocal對象和多個ThreadLocal對象對代碼本身沒什么影響,實在沒必要重復創建多個對象。
- 延長ThreadLocal的生命周期,方便使用。
網上很多地方把static和內存泄漏聯系起來,原諒我沒看出來這兩者有什么關系。
最后來到第四個問題,也大家都關心的內存泄漏啦,。
通過上面的引用關系圖我們了解到存在兩個引用關系,分別是key的弱引用和value的強引用。弱引用首選不可能導致內存泄漏,因為gc發生時弱引用的對象就有可能被回收了。所以。。。內存泄漏發生在強引用這個關系上。
因為現在線程切換的開銷比較大,大家現在普遍使用線程池的技術去避免線程的頻繁創建。在線程池中,線程不會消亡,會被重復使用,so。。。。上邊的強引用得不到釋放了,內存泄漏就這樣發生了。其實我在JDK8上看到的是java已經為此做了一些工作了,比如執行下次set操作時遍歷key是null的Entry對象並釋放value的引用。雖然java本身做了一些工作,仍然強烈建議使用完ThreadLocal執行remove方法主動消除引用關系。
文章結束了,如有紕漏,歡迎指出。