- 前言
- 一、了解ThreadLocal的作用
- 二、ThreadLocal簡單使用
- 三、ThreadLocal原理
- 3.1 ThreadLocal的存取過程
- 3.2 探究ThreadLocalMap對象
- 3.3 ThreadLocal對象的回收
- 四、ThreadLocal應用場景
前言
ThreadLocal是多線程處理中非常重要的一個工具,比如數據庫連接池存放Connection、存放本地參數等作用,面試也經常會問到它的應用及原理,本文就將從外到內地學習一下ThreadLocal。
一、了解ThreadLocal的作用
ThreadLocal顧名思義是線程私有的局部變量存儲容器,可以理解成每個線程都有自己專屬的存儲容器,它用來存儲線程私有變量,其實它只是一個外殼,內部真正存取是一個Map,后面會仔細講解。每個線程可以通過set()
和get()
存取變量,多線程間無法訪問各自的局部變量,相當於在每個線程間建立了一個隔板。只要線程處於活動狀態,它所對應的ThreadLocal實例就是可訪問的,線程被終止后,它的所有實例將被垃圾收集。總之記住一句話:ThreadLocal存儲的變量屬於當前線程。
二、ThreadLocal簡單使用
話不多說先看一下ThreadLocal的一個簡單案例
Code:
public class Test implements Runnable { private static AtomicInteger counter = new AtomicInteger(100); private static ThreadLocal<String> threadInfo = new ThreadLocal<String>() { @Override protected String initialValue() { return "[" + Thread.currentThread().getName() + "," + counter.getAndIncrement() + "]"; } }; @Override public void run() { System.out.println("threadInfo value:" + threadInfo.get()); } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(new Test()); Thread thread2 = new Thread(new Test()); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("threadInfo value in main:" + threadInfo.get()); } }
Output:
threadInfo value:[Thread-0,100] threadInfo value:[Thread-1,101] threadInfo value in main:[main,102]
上述代碼中我用ThreadLocal來存儲線程的信息,其格式為[線程名,線程ID]
,定義的變量是靜態的。從運行結果可以看出來每個線程包括主線程訪問到的threadInfo
獲取到的值都是不一樣的,而且存放的信息就是本線程的信息,也應證了上面那句話ThreadLocal存儲的變量屬於當前線程。
三、ThreadLocal原理
3.1 ThreadLocal的存取過程
解析原理先從源碼開始,首先看一下ThreadLocal.set()
方法
// ThreadLocal中set方法
public void set(T value) { // 獲取當前線程對象 Thread t = Thread.currentThread(); // 獲取該線程的threadLocals屬性(ThreadLocalMap對象) ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } // Thread類中的threadLocals定義 ThreadLocal.ThreadLocalMap threadLocals = null; // ThreadLocal中getMap方法 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } // ThreadLocal中createMap方法 // 為線程創建ThreadLocalMap對象並賦值給threadLocals void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
從源碼中看到在set方法里面就是把值存入ThreadLocalMap
類中,這個類是屬於ThreadLocal
的內部類,但是在Thread
類中也有定義threadLocals
變量,get方法的操作對象也是ThreadLocalMap
,也就是說關鍵的存儲和獲取實質上在於ThreadLocalMap
類。其中是以ThreadLocal類為key,存入的值為value,而ThreadLocal又是定義在每個線程的屬性中,這也就實現了“ThreadLocal線程私有化”的功能,每次都是先從當前線程獲取到threadLocals屬性,也就是獲得ThreadLocalMap對象,以ThreadLocal對象作為key存取對應的值。
3.2 探究ThreadLocalMap對象
ThreadLocalMap
對象是ThreadLocal類的內部類,其中它就是一個簡單的Map,每個存的值被封裝成Entry進行存儲,下面是Entry的源碼
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
Entry是ThreadLocalMap的內部類,仔細觀察其源碼發現,它是繼承了一個ThreadLocal的弱引用。回憶一下Java中的四種引用:強引用、軟引用、弱引用、幻象引用。強引用是new創建出來的對象,只要強引用存在,垃圾收集器永遠不會回收該引用;軟引用(SoftReference)是在內存即將被占滿時被回收;弱引用(WeakReference)用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次GC發生之前,當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉該類對象;幻象引用又稱虛引用或幽靈引用(Phantom Reference),它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得對象實例,任何時候都可能被回收。更多的解釋和示例讀者可前往我之前寫的這篇文章閱讀:
3.3 ThreadLocal對象的回收
Entry是一個弱引用,是因為它不會影響到ThreadLocal的GC行為,如果是強引用的話,在線程運行過程中,我們不再使用ThreadLocal了,將ThreadLocal置為null,但ThreadLocal在線程的ThreadLocalMap里還有引用,導致其無法被GC回收。而Entry聲明為WeakReference,ThreadLocal置為null后,線程的ThreadLocalMap就不算強引用了,ThreadLocal就可以被GC回收了。
// ThreadLocal中的remove方法
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } // ThreadLocalMap中的remove方法 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) { e.clear(); expungeStaleEntry(i); return; } } }
可能存在的內存泄漏問題
ThreadLocalMap的生命周期是與線程一樣的,但是ThreadLocal卻不一定,可能ThreadLocal使用完了就想要被回收,但是此時線程可能不會立即終止,還會繼續運行(比如線程池中線程重復利用),如果ThreadLocal對象只存在弱引用,那么在下一次垃圾回收的時候必然會被清理掉。
如果ThreadLocal沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來ThreadLocalMap中使用這個ThreadLocal的key也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現key為null的value
在ThreadLocalMap中,調用 set()、get()、remove()方法的時候,會清理掉key為null的記錄。在ThreadLocal設置為null之后,ThreadLocalMap中存在key為null的值,那么就可能發生內存泄漏,只有手動調用remove()
方法來避免,所以我們在使用完ThreadLocal變量時,盡量用threadLocal.remove()來清除,避免threadLocal=null的操作。remove方法是徹底地回收該對象,而通過threadLocal=null
只是釋放掉了ThreadLocal的引用,但是在ThreadLocalMap中卻還存在其Entry,后續還需處理。
四、ThreadLocal應用場景
- 處理較為復雜的業務時,使用ThreadLocal代替參數的顯示傳遞。
- ThreadLocal可以用來做數據庫連接池保存Connection對象,這樣就可以讓線程內多次獲取到的連接是同一個了(Spring中的DataSource就是使用的ThreadLocal)。
- 管理Session會話,將Session保存在ThreadLocal中,使線程處理多次處理會話時始終是一個Session。