想必不少小夥伴們對ThreadLocal
並不陌生,ThreadLocal
叫作線程本地變量,也就是ThreadLocal爲變量在每一個線程中都建立了一個副本,每一個線程能夠訪問本身內部的副本變量。那麼,咱們使用ThreadLocal必定線程安全麼?話很少說,先上結論:java
若是threadlocal.get以後的副本,只在當前線程中使用,那麼是線程安全的;若是對其餘線程暴露,不必定是線程安全的。git
爲了演示下錯誤的使用方式,先看下以下代碼(雖然小夥伴們都不會這樣寫代碼 ^_^):github
static class Container { int num; } public static void main(String[] args) throws InterruptedException { ThreadLocal<Container> tl = new ThreadLocal<>(); tl.set(new Container()); // 先set下ThreadLocal Container container = tl.get(); Runnable task = () -> { for (int i = 0; i < 10000; i++) { container.num++; } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(tl.get().num); }
筆者的一次結果輸出爲:17581數組
結合代碼,咱們知道,在執行threadlcoal.get
獲取到線程變量副本以後,不要讓其餘線程來訪問它了,不然就是多線程操做同一個變量,可能形成線程安全問題。安全
除了上述討論的ThreadLocal線程安全性問題以外,ThreadLocal若是使用不當,可能存在內存泄露問題。ThreadLocal變量是保存在Thread.threadLocals
中(ThreadLocalMap類型)以Entry類型保存的,其中Entry.key(也就是弱引用referent實際指向對象)爲ThreadLocal變量,該變量爲弱類型;Entry.value爲實際set的value。多線程
// Entry,裏面保存在ThreadLocal變量,也就是key,是弱引用 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
雖然Entry.referent是弱類型,指向ThreadLocal變量,可是若是ThreadLocal變量自己引用不置爲null的話,這裏的Entry.referent指向對象是不會釋放的。好比咱們經常使用的定義方式:app
// 靜態變量和對象屬性 static ThreadLocal<String> tls = new ThreadLocal<>(); ThreadLocal<Integer> tli = new ThreadLocal<>();
相似於靜態變量和對象屬性這種引用,若是不將tls或tli設置爲null,那麼ThreadLocal變量沒法釋放(這不是廢話麼,人家但是強引用呀),此時的Entry.referent弱類型沒啥卵用;只有在tls或tli爲null時,Entry.referent弱類型就起做用了,在第一次GC時就會將Entry.referent弱類型指向的對象回收。性能
若是Entry.referent弱類型指向的對象回收了(沒調用ThreadLocal.remove操做),Entry.value對象還在,而且Entry.value但是強引用的,此時就發生了內存泄露。這也就是ThreadLocal使用不當(沒調用ThreadLocal.remove)時產生的內存泄漏問題。不過,伴隨着其餘ThreadLocal對象的set/get/remove
的進行,會清除一部分Entry.referent爲null可是Entry.value不爲null的對象的,也就是修復內存泄露問題,注意,這個只是清除部分這樣的Entry,並不能保證一次就能清除所有這樣的Entry,因此仍是要遵循ThreadLocal.set,用完以後就remove。優化
討論完了ThreadLocal的潛在問題以後,你是否是意猶未盡,想深刻了解下ThreadLocal實現原理
?OK,那就搬起小板凳,一塊兒嘮嘮吧~this
ps:若是小夥伴對ThreadLocal原理已經熟悉了,那麼恭喜你,後面的內容能夠不看了~
ThreadLocal變量主要有get/set/remove
三個操做,理解了這三個操做流程,基本上就理解了ThreadLocal實現原理。
get流程以下:
/** * 當前線程的threadLocalMap中獲取當前ThreadLocal對應的value */ 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; } } // 設置null值,下次直接返回null了 return setInitialValue(); } /** * 若是一次找到了entry,直接返回;不然就是set時hash衝突了 * 遍歷後續的slot,進行查找 * 這裏其實JDK能夠作個優化,在set以後,將slot位置記錄在Threadlocal變量中,下次直接到對應slot位置get便可 */ 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); }
注意:線程的threadLocals是一個基於開放定址法實現的map結構。
INITIAL_CAPACITY = 16
,默認擴容閾值爲threshold = INITIAL_CAPACITY * 2 / 3
,擴容時按照成倍擴容。/** * 獲取當前線程的threadLocalMap,非空直接set value; * 不然新建一個包含value的threadLocalMap。 * threadLocalMap的key對應程序中定義的ThreadLocal變量,value對應要set的值 */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // Thread.threadLocals if (map != null) map.set(this, value); else createMap(t, value); } // Entry,裏面保存在ThreadLocal變量,也就是key,是弱引用 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } /** * hash碼的生成,這裏全部的ThreadLocal對象hash生成都是基於static變量nextHashCode來作的 * 建立ThreadLocal對象時threadLocalHashCode已初始化完成 */ private final int threadLocalHashCode = nextHashCode(); private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } private static AtomicInteger nextHashCode = new AtomicInteger(); /** * 當前線程的threadLocalMap非空直接set value */ private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); // 若是當前table[i] hash衝突,那麼就以i爲起點,遍歷後續table[i], // 這其實就是hash衝突中的開放定址法,另一種是分離連接法 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // key已存在,更新vlaue便可 if (k == key) { e.value = value; return; } // key爲null,複製value便可 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 新建Entry,清理一部分Entry.key爲null,value不爲null的數據,避免內存泄露 // 超過了threshold時rehash操做 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
/** * 從ThreadLocalMap刪除對應key */ public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } private void remove(ThreadLocal<?> key) { 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)]) { if (e.get() == key) { // 清除Entry.key弱引用,設置爲null e.clear(); // 清除Entry.value引用,可能還涉及部分key爲null的Entry數據清理 expungeStaleEntry(i); return; } } }
小結
從ThreadLocal的get/set
操做流程來看,ThreadLocal的value 是 Lazy Init(延遲初始化的)
。ThreadLocal爲何是延遲初始化,這個問題應該是容易理解的,緣由是:在沒有具體業務場景前提下,這樣的作法避免內存浪費。
ThreadLocal變量默認放在基於開放定址法實現的map結構中,這種結構在hash衝突時會形成屢次get/set
操做,理論上能夠經過記錄ThreadLocal變量set時的位置,這樣下次直接經過該位置獲取對應value便可,能夠參考netty的FastThreadLocal
,它的實現思路就是這樣的,提升了set/get的效率。
最後來一張ThreadLocal的總體圖:
參考資料:
一、https://luoxn28.github.io/2019/04/27/ni-de-threadlocal-yi-ding-xian-cheng-an-quan-ma/