深入理解ThreadLocal底層原理

ThreadLocal簡介

變量值的共享可以使用public static的形式,所有線程都使用同一個變量,如果想實現每一個線程都有自己的共享變量該如何實現呢?JDK中的ThreadLocal類正是爲了解決這樣的問題。

ThreadLocal類並不是用來解決多線程環境下的共享變量問題,而是用來提供線程內部的共享變量,在多線程環境下,可以保證各個線程之間的變量互相隔離、相互獨立。在線程中,可以通過get()/set()方法來訪問變量。ThreadLocal實例通常來說都是private static類型的,它們希望將狀態與線程進行關聯。這種變量在線程的生命週期內起作用,可以減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的複雜度。

我們先通過一個例子來看一下ThreadLocal的基本用法:

  1. public class ThreadLocalTest {
  2. static class MyThread extends Thread {
  3. private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
  4. @Override
  5. public void run() {
  6. super.run();
  7. for (int i = 0; i < 3; i++) {
  8. threadLocal.set(i);
  9. System.out.println(getName() + " threadLocal.get() = " + threadLocal.get());
  10. }
  11. }
  12. }
  13. public static void main(String[] args) {
  14. MyThread myThreadA = new MyThread();
  15. myThreadA.setName("ThreadA");
  16. MyThread myThreadB = new MyThread();
  17. myThreadB.setName("ThreadB");
  18. myThreadA.start();
  19. myThreadB.start();
  20. }
  21. }

運行結果(不唯一):

ThreadA threadLocal.get() = 0
ThreadB threadLocal.get() = 0
ThreadA threadLocal.get() = 1
ThreadA threadLocal.get() = 2
ThreadB threadLocal.get() = 1
ThreadB threadLocal.get() = 2

雖然兩個線程都在向threadLocal對象中set()數據值,但每個線程都還是能取出自己設置的數據,確實可以達到隔離線程變量的效果。

ThreadLocal源碼解析

ThreadLocal常用方法介紹


get()方法:獲取與當前線程關聯的ThreadLocal值。

set(T value)方法:設置與當前線程關聯的ThreadLocal值。

initialValue()方法:設置與當前線程關聯的ThreadLocal初始值。

當調用get()方法的時候,若是與當前線程關聯的ThreadLocal值已經被設置過,則不會調用initialValue()方法;否則,會調用initialValue()方法來進行初始值的設置。通常initialValue()方法只會被調用一次,除非調用了remove()方法之後又調用get()方法,此時,與當前線程關聯的ThreadLocal值處於沒有設置過的狀態(其狀態體現在源碼中,就是線程的ThreadLocalMap對象是否爲null),initialValue()方法仍會被調用。

initialValue()方法是protected類型的,很顯然是建議在子類重載該函數的,所以通常該方法都會以匿名內部類的形式被重載,以指定初始值,例如:

  1. public class ThreadLocalTest {
  2. public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
  3. @Override
  4. protected Integer initialValue() {
  5. return Integer.valueOf(1);
  6. }
  7. };
  8. }

remove()方法:將與當前線程關聯的ThreadLocal值刪除。

實現原理

ThreadLocal最簡單的實現方式就是ThreadLocal類內部有一個線程安全的Map,然後用線程的ID作爲Map的key,實例對象作爲Map的value,這樣就能達到各個線程的值隔離的效果。

JDK最早期的ThreadLocal就是這樣設計的,但是,之後ThreadLocal的設計換了一種方式,我們先看get()方法的源碼,然後進一步介紹ThreadLocal的實現方式:

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. ThreadLocalMap.Entry e = map.getEntry(this);
  6. if (e != null) {
  7. @SuppressWarnings("unchecked")
  8. T result = (T)e.value;
  9. return result;
  10. }
  11. }
  12. return setInitialValue();
  13. }

get()方法主要做了以下事情:

1、調用Thread.currentThread()獲取當前線程對象t;

2、根據當前線程對象,調用getMap(Thread)獲取線程對應的ThreadLocalMap對象:

  1. ThreadLocalMap getMap(Thread t) {
  2. return t.threadLocals;
  3. }

threadLocals是Thread類的成員變量,初始化爲null:

  1. /* ThreadLocal values pertaining to this thread. This map is maintained
  2. * by the ThreadLocal class. */
  3. ThreadLocal.ThreadLocalMap threadLocals = null;

3、如果獲取的map不爲空,則在map中以ThreadLocal的引用作爲key來在map中獲取對應的value e,否則轉到步驟5;

4、若e不爲null,則返回e中存儲的value值,否則轉到步驟5;

5、調用setInitialValue()方法,對線程的ThreadLocalMap對象進行初始化操作,ThreadLocalMap對象的key爲ThreadLocal對象,value爲initialValue()方法的返回值。

從上面的分析中,可以看到,ThreadLocal的實現離不開ThreadLocalMap類,ThreadLocalMap類是ThreadLocal的靜態內部類。每個Thread維護一個ThreadLocalMap映射表,這個映射表的key是ThreadLocal實例本身,value是真正需要存儲的Object。這樣的設計主要有以下幾點優勢:

  • 這樣設計之後每個Map的Entry數量變小了:之前是Thread的數量,現在是ThreadLocal的數量,能提高性能;
  • 當Thread銷燬之後對應的ThreadLocalMap也就隨之銷燬了,能減少內存使用量。

ThreadLocalMap源碼分析

ThreadLocalMap是用來存儲與線程關聯的value的哈希表,它具有HashMap的部分特性,比如容量、擴容閾值等,它內部通過Entry類來存儲key和value,Entry類的定義爲:

  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2. /** The value associated with this ThreadLocal. */
  3. Object value;
  4. Entry(ThreadLocal<?> k, Object v) {
  5. super(k);
  6. value = v;
  7. }
  8. }

Entry繼承自WeakReference,通過上述源碼super(k);可以知道,ThreadLocalMap是使用ThreadLocal的弱引用作爲Key的。

分析到這裏,我們可以得到下面這個對象之間的引用結構圖(其中,實線爲強引用,虛線爲弱引用):


我們知道,弱引用對象在Java虛擬機進行垃圾回收時,就會被釋放,那我們考慮這樣一個問題:

ThreadLocalMap使用ThreadLocal的弱引用作爲key,如果一個ThreadLocal沒有外部關聯的強引用,那麼在虛擬機進行垃圾回收時,這個ThreadLocal會被回收,這樣,ThreadLocalMap中就會出現key爲null的Entry,這些key對應的value也就再無妨訪問,但是value卻存在一條從Current Thread過來的強引用鏈。因此只有當Current Thread銷燬時,value才能得到釋放。

該強引用鏈如下:

CurrentThread Ref -> Thread -> ThreadLocalMap -> Entry -> value

因此,只要這個線程對象被gc回收,那些key爲null對應的value也會被回收,這樣也沒什麼問題,但在線程對象不被回收的情況下,比如使用線程池的時候,核心線程是一直在運行的,線程對象不會回收,若是在這樣的線程中存在上述現象,就可能出現內存泄露的問題。

那在ThreadLocalMap中是如何解決這個問題的呢?

在獲取key對應的value時,會調用ThreadLocalMap的getEntry(ThreadLocal<?> key)方法,該方法源碼如下:

  1. private Entry getEntry(ThreadLocal<?> key) {
  2. int i = key.threadLocalHashCode & (table.length - 1);
  3. Entry e = table[i];
  4. if (e != null && e.get() == key)
  5. return e;
  6. else
  7. return getEntryAfterMiss(key, i, e);
  8. }

通過key.threadLocalHashCode & (table.length - 1)來計算存儲key的Entry的索引位置,然後判斷對應的key是否存在,若存在,則返回其對應的value,否則,調用getEntryAfterMiss(ThreadLocal<?>, int, Entry)方法,源碼如下:

  1. private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. while (e != null) {
  5. ThreadLocal<?> k = e.get();
  6. if (k == key)
  7. return e;
  8. if (k == null)
  9. expungeStaleEntry(i);
  10. else
  11. i = nextIndex(i, len);
  12. e = tab[i];
  13. }
  14. return null;
  15. }

ThreadLocalMap採用線性探查的方式來處理哈希衝突,所以會有一個while循環去查找對應的key,在查找過程中,若發現key爲null,即通過弱引用的key被回收了,會調用expungeStaleEntry(int)方法,其源碼如下:

  1. private int expungeStaleEntry(int staleSlot) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. // expunge entry at staleSlot
  5. tab[staleSlot].value = null;
  6. tab[staleSlot] = null;
  7. size--;
  8. // Rehash until we encounter null
  9. Entry e;
  10. int i;
  11. for (i = nextIndex(staleSlot, len);
  12. (e = tab[i]) != null;
  13. i = nextIndex(i, len)) {
  14. ThreadLocal<?> k = e.get();
  15. if (k == null) {
  16. e.value = null;
  17. tab[i] = null;
  18. size--;
  19. } else {
  20. int h = k.threadLocalHashCode & (len - 1);
  21. if (h != i) {
  22. tab[i] = null;
  23. // Unlike Knuth 6.4 Algorithm R, we must scan until
  24. // null because multiple entries could have been stale.
  25. while (tab[h] != null)
  26. h = nextIndex(h, len);
  27. tab[h] = e;
  28. }
  29. }
  30. }
  31. return i;
  32. }

通過上述代碼可以發現,若key爲null,則該方法通過下述代碼來清理與key對應的value以及Entry:

  1. // expunge entry at staleSlot
  2. tab[staleSlot].value = null;
  3. tab[staleSlot] = null;

此時,CurrentThread Ref不存在一條到Entry對象的強引用鏈,Entry到value對象也不存在強引用,那在程序運行期間,它們自然也就會被回收。expungeStaleEntry(int)方法的後續代碼就是以線性探查的方式,調整後續Entry的位置,同時檢查key的有效性。

在ThreadLocalMap中的set()/getEntry()方法中,都會調用expungeStaleEntry(int)方法,但是如果我們既不需要添加value,也不需要獲取value,那還是有可能產生內存泄漏的。所以很多情況下需要使用者手動調用ThreadLocal的remove()函數,手動刪除不再需要的ThreadLocal,防止內存泄露。若對應的key存在,remove()方法也會調用expungeStaleEntry(int)方法,來刪除對應的Entry和value。

其實,最好的方式就是將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命週期就更長,由於一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然後remove它,可以防止內存泄露。

InheritableThreadLocal

InheritableThreadLocal繼承自ThreadLocal,使用InheritableThreadLocal類可以使子線程繼承父線程的值,來看一段示例代碼:

  1. public class ThreadLocalTest {
  2. private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() {
  3. @Override
  4. protected Integer initialValue() {
  5. return Integer.valueOf(10);
  6. }
  7. };
  8. static class MyThread extends Thread {
  9. @Override
  10. public void run() {
  11. super.run();
  12. System.out.println(getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get());
  13. }
  14. }
  15. public static void main(String[] args) {
  16. System.out.println(Thread.currentThread().getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get());
  17. MyThread myThread = new MyThread();
  18. myThread.setName("線程A");
  19. myThread.start();
  20. }
  21. }

運行結果:

main inheritableThreadLocal.get() = 10
線程A inheritableThreadLocal.get() = 10

可以看到子線程成功繼承了父線程的值。

父線程還可以設置子線程的初始值,只需要重寫InheritableThreadLocal類的childValue(T)方法即可,將上述代碼的inheritableThreadLocal 定義修改爲如下方式:

  1. private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() {
  2. @Override
  3. protected Integer initialValue() {
  4. return Integer.valueOf(10);
  5. }
  6. @Override
  7. protected Integer childValue(Integer parentValue) {
  8. return Integer.valueOf(5);
  9. }
  10. };

運行結果爲:

main inheritableThreadLocal.get() = 10
線程A inheritableThreadLocal.get() = 5

可以看到,子進程成功獲取到了父進程設置的初始值。

使用InheritableThreadLocal類需要注意的一點是,如果子線程在取得值的同時,主線程將InheritableThreadLocal中的值進行更改,那子線程獲取的還是舊值。

線程中用來實現上述功能的ThreadLocalMap類變量爲

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

InheritableThreadLocal類的實現很簡單,主要是重寫了ThreadLocal類的getMap(Thread)方法和createMap(Thread, T)方法,將其中操作的ThreadLocalMap變量修改爲了inheritableThreadLocals,這裏不再進一步敘述。

參考資料

高洪巖:《Java多線程編程核心技術》

ThreadLocal和synchronized的區別

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章